I was migrating a client's blog from a static site generator to Sanity CMS when I realized I'd be copying and pasting dozens of markdown articles manually. After spending hours researching the Sanity API and testing different approaches, I built a CLI script that automatically transforms markdown files with frontmatter into properly structured Sanity documents. This guide shows you exactly how to create your own markdown import tool that handles everything from basic posts to complex How-To guides with FAQs.
The Problem with Manual Content Migration
Moving content between systems typically involves tedious manual work. You have markdown files with frontmatter containing all your metadata, but Sanity expects structured documents with specific field mappings. Copying content manually means losing time and introducing errors, especially when you need to create related documents like FAQ items that reference back to your main posts.
The solution I developed parses YAML frontmatter, maps fields to your Sanity schema, handles complex content types, and provides proper error handling for production use. By the end of this guide, you'll have a robust CLI tool that transforms markdown files into Sanity documents with a single command.
Setting Up the Foundation
The script requires gray-matter to parse frontmatter and the Sanity client for API operations. Start by installing the necessary dependency in your existing Sanity project.
bash
# Install gray-matter for frontmatter parsing
pnpm add gray-matter
Next, create the scripts directory structure to organize your automation tools.
bash
# Create scripts directorymkdir -p scripts
This approach keeps your CLI tools separate from your main application code while making them easily accessible through package.json scripts. The gray-matter package specifically handles the complex task of separating YAML frontmatter from markdown content, which forms the core of our parsing logic.
Building the Core Script Structure
The script needs TypeScript interfaces that match your Sanity schema and handle the various content types you'll encounter. Create the main script file with comprehensive type definitions.
These interfaces define the expected structure of your frontmatter and ensure type safety throughout the script. The FrontMatter interface maps directly to your Sanity post schema, while FAQ and HowToStep interfaces handle the more complex content types. Notice how the interfaces are optional for most fields, allowing flexibility in your markdown files while enforcing required fields through validation.
Implementing Sanity Client Integration
The script needs robust connection handling and environment variable validation to work reliably across different environments. Add the client setup with proper error handling.
typescript
// File: scripts/push-markdown.ts (continued)functioncalculateReadingTime(content: string): number {
const words = content.trim().split(/\s+/).lengthreturnMath.ceil(words / 200)
}
functiongenerateSlugFromFilename(filename: string): string {
return path.basename(filename, '.md')
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '')
}
asyncfunctionpushMarkdownToSanity(filePath: string) {
try {
if (!fs.existsSync(filePath)) {
console.error(`❌ File not found: ${filePath}`)
process.exit(1)
}
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_IDconst dataset = process.env.NEXT_PUBLIC_SANITY_DATASETconst token = process.env.SANITY_API_TOKENif (!projectId || !dataset || !token) {
console.error('❌ Missing required environment variables:')
if (!projectId) console.error(' - NEXT_PUBLIC_SANITY_PROJECT_ID')
if (!dataset) console.error(' - NEXT_PUBLIC_SANITY_DATASET')
if (!token) console.error(' - SANITY_API_TOKEN')
console.error('\nAdd these to your .env file or set them as environment variables.')
process.exit(1)
}
const client = createClient({
projectId,
dataset,
useCdn: false,
token,
apiVersion: '2023-05-03'
})
const fileContent = fs.readFileSync(filePath, 'utf8')
const { data: frontmatter, content: markdownContent } = matter(fileContent)
const typedFrontmatter = frontmatter asFrontMatter
This setup validates all required environment variables before attempting any API calls, preventing confusing error messages later. The client configuration uses useCdn: false because we're writing data, not just reading it. The gray-matter library cleanly separates the frontmatter from the markdown content, giving us structured data to work with.
Adding Comprehensive Validation
Content validation prevents incomplete or malformed documents from being created in Sanity. Implement field validation that matches your schema requirements.
The validation uses TypeScript's keyof operator to ensure type safety when checking required fields. If any required fields are missing, the script exits early with a clear error message. The slug generation fallback uses the filename when no explicit slug is provided, ensuring every document has a unique identifier.
Creating FAQ Documents and References
FAQ items are separate documents in Sanity that get referenced by posts. This approach allows FAQs to be reused across multiple articles and managed independently.
The FAQ creation function generates unique IDs based on the post slug and FAQ index, ensuring no conflicts between posts. Each FAQ answer is converted to Sanity's block content format, which allows for rich text formatting in the Sanity Studio interface. The function returns the created FAQ IDs so they can be referenced in the main post document.
Handling How-To Steps as Embedded Objects
How-To steps are embedded directly within posts rather than being separate documents. This structure makes them easier to manage as part of the article content while still providing structured schema markup benefits.
This approach creates the FAQ documents first, then processes the How-To steps as embedded objects. The step parsing converts simple text instructions into Sanity's block content format, maintaining consistency with how other rich text content is stored. The conditional URL inclusion demonstrates how optional fields are handled cleanly.
Building the Complete Document Structure
The final document creation combines all the parsed data into a structure that matches your Sanity schema. This includes handling both new document creation and updates to existing documents.
The document structure uses Sanity's reference format for relationships and includes optional field spreading to avoid undefined values. The update-or-create logic checks for existing posts by slug, making the script idempotent. New posts are created as drafts with timestamp-based IDs, allowing you to add images manually before publishing.
Adding CLI Integration and Error Handling
The final step makes the script accessible as a command-line tool with proper argument handling and helpful usage information.
The CLI integration provides helpful usage information when no arguments are provided and resolves file paths properly regardless of where the command is run from. This makes the tool user-friendly and prevents common path-related errors.
Testing Your Implementation
Create a sample markdown file to verify everything works correctly with various content types:
yaml
# File: content/posts/example-post.md---title:"Sample How-To Guide"authorId:"your-author-id"publishedAt:"2025-08-11T12:00:00Z"isHowTo:truedifficulty:"intermediate"faqs:-question:"Does this really work?"answer:"Yes, it processes all content types correctly."category:"technical"steps:-name:"First step"text:"This demonstrates step parsing"-name:"Second step"text:"This includes a reference URL"url:"https://example.com"---
# Your markdown content hereThiscontentwillbeimportedalongwithallthestructuredmetadata.
Run the import command to test your implementation:
bash
pnpm run push-md ./content/posts/example-post.md
The script will create FAQ documents first, then the main post with all embedded How-To steps and references properly structured. You can then open Sanity Studio to add images and publish the imported content.
Conclusion
This CLI script solves the tedious problem of manually migrating markdown content to Sanity CMS by automating the entire import process. The solution parses YAML frontmatter, creates complex document relationships, and handles both simple posts and advanced content types like How-To guides with FAQs. You now have a robust tool that transforms markdown files into properly structured Sanity documents with a single command, saving hours of manual work and eliminating migration errors.
The script handles validation, error cases, and provides clear feedback throughout the import process, making it suitable for production use. Whether you're migrating existing content or establishing a workflow for future articles, this approach streamlines content management while preserving the flexibility of markdown authoring.
Let me know in the comments if you have questions about extending the script for your specific schema, and subscribe for more practical development guides.