
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag storeGreat question! Converting markdown to Portable Text directly is generally the better path rather than going markdown → HTML → Portable Text. Here's why and how to approach it:
Going directly from markdown to Portable Text preserves the semantic structure better and avoids introducing HTML-specific quirks. The markdown → HTML → Portable Text route adds an unnecessary transformation step that can introduce edge cases.
@sanity/block-tools (now also published as @portabletext/block-tools) is your primary library here. It includes htmlToBlocks which works with the Ghost guide you linked, but more importantly for your case, you can use it with remark/rehype to process markdown directly.
The typical workflow looks like:
htmlToBlocks from @sanity/block-tools)Here's a basic implementation:
import {unified} from 'unified'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import rehypeStringify from 'rehype-stringify'
import {htmlToBlocks} from '@sanity/block-tools'
import {Schema} from '@sanity/schema'
import {JSDOM} from 'jsdom'
// Load your Sanity schema
const defaultSchema = Schema.compile({
name: 'default',
types: [/* your schema types */]
})
const blockContentType = defaultSchema
.get('blockContent') // or whatever your PT field is called
.jsonType
async function markdownToPortableText(markdown) {
// Convert markdown to HTML via remark
const file = await unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypeStringify)
.process(markdown)
const html = String(file)
// Convert HTML to Portable Text blocks
const blocks = htmlToBlocks(html, blockContentType, {
parseHtml: (htmlString) => new JSDOM(htmlString).window.document
})
return blocks
}As mentioned in the content migration principles, migrations should be incremental and idempotent. Converting directly from markdown:
The @sanity/block-tools package handles basic rich text formatting like headings, paragraphs, and lists automatically. You can also add custom deserialization rules to handle specific markdown patterns that need special treatment (like code blocks, images, or custom syntax).
If you want to keep markdown as-is in Sanity and render it later, consider sanity-plugin-markdown. This stores markdown natively and provides a markdown editor in the Studio. However, you lose the powerful querying benefits of Portable Text.
The guide you linked is solid for HTML sources (like Ghost exports), but since you're starting with markdown, the remark/rehype ecosystem gives you a mature, well-maintained foundation for your migration scripts. The intermediate HTML step is quick and leverages the robust htmlToBlocks function that already handles edge cases well.
Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.
Content operations
Content backend


The only platform powering content operations
By Industry


Tecovas strengthens their customer connections
Build and Share

Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag store