Markdown to Sanity
What to Determine First
Before writing migration code, determine:
- Source shape: Markdown files, MDX, GitHub README-style content, static-site content, AI-generated Markdown, or Markdown exported from a CMS.
- File count, directory structure, and whether paths encode slugs, dates, sections, or locales.
- Frontmatter fields and formats.
- Markdown extensions: GFM tables, callouts, directives, footnotes, raw HTML, code fences, embeds, local images, and MDX JSX.
- Target Sanity document type and Portable Text field name.
- Allowed Portable Text styles, lists, decorators, annotations, and custom object types.
- Whether tags, authors, categories, and images are references, inline fields, or separate documents.
Default Conversion Path
Use direct Markdown -> Portable Text conversion with @portabletext/markdown.
npm install @portabletext/markdownimport {markdownToPortableText} from '@portabletext/markdown'
const body = markdownToPortableText(markdown)Do not call Markdown -> HTML -> htmlToBlocks "direct conversion." That is an HTML fallback path. Use it only when the Markdown pipeline cannot handle source-specific syntax that is easier to normalize as HTML.
Schema-Constrained Conversion
For a Sanity block array schema, convert it with @portabletext/sanity-bridge:
npm install @portabletext/markdown @portabletext/sanity-bridgeimport {markdownToPortableText} from '@portabletext/markdown'
import {sanitySchemaToPortableTextSchema} from '@portabletext/sanity-bridge'
const schema = sanitySchemaToPortableTextSchema(sanityBlockArraySchema)
const body = markdownToPortableText(markdown, {schema})Use custom matchers when Markdown syntax must become specific Sanity objects:
const body = markdownToPortableText(markdown, {
schema,
types: {
table: ({context, value}) => ({
_type: 'table',
_key: context.keyGenerator(),
rows: value.rows,
headerRows: value.headerRows,
}),
},
})Only emit custom types that exist in the target schema.
Frontmatter Mapping
Use a frontmatter parser such as gray-matter:
npm install gray-matterMap common fields deliberately:
title-> document title.slugor file path ->slug.current.date,published,updated-> explicit datetime fields such aspublishedAt.author-> reference, string, or author document depending on target schema.tagsandcategories-> reference documents when they power filtering/navigation.description,canonical,ogImage-> SEO object if the target schema has one.- Source file path -> migration metadata for debugging.
Do not put frontmatter into the Portable Text body unless it is actual editorial content.
Asset Handling
Markdown images need explicit handling:
- Remote image: use
_sanityAsset: "image@https://..."in NDJSON or upload through the client. - Local image: use an absolute
file:///...URI in NDJSON, package assets into a tarball, or upload through the client. - Prefer the largest original image, not resized derivatives.
- Preserve alt text from
and title text when present. - Rewrite image paths before conversion if Markdown uses relative paths.
- Log unresolved images; do not silently keep broken local paths.
If images should remain inline inside Portable Text, ensure the Portable Text schema allows an image object. If images belong in a separate field such as mainImage, extract them before conversion.
Document IDs and Import
Use stable IDs:
- File path slug:
post-my-folder-my-file. - Frontmatter ID:
post-<sourceId>. - Hash only when no stable source ID/path exists.
For bulk imports, prefer NDJSON:
{"_id":"post-example","_type":"post","title":"Example","body":[{"_type":"block","_key":"a","style":"normal","children":[{"_type":"span","_key":"b","text":"Hello","marks":[]}],"markDefs":[]}]}Then import with:
npx sanity datasets import import.ndjson <dataset> --replaceUse client createOrReplace for smaller imports or incremental syncs.
Keep Markdown Native
Only keep Markdown native when the project intentionally wants Markdown editing/rendering instead of Portable Text querying and structured blocks.
If keeping Markdown native, use a Markdown field/plugin and document the tradeoff: simpler migration and editing for Markdown-first teams, but weaker structured querying, references, annotations, and block-level content reuse.
Validation Checklist
- Confirm every document has
_id,_type, title/slug where required, and a Portable Text array. - Confirm no body field is a raw Markdown string unless the target intentionally stores native Markdown.
- Confirm all Portable Text custom
_typevalues exist in the schema. - Confirm all image references resolve to remote URLs, absolute local file URLs, or uploaded Sanity asset IDs.
- Confirm frontmatter dates parse to valid ISO datetime strings.
- Confirm tags/authors/categories resolve according to the target model.
- Confirm rerunning the transform produces the same IDs.
- Confirm MDX JSX is mapped to Sanity objects, stripped, or logged for manual cleanup.