Skip to content

Markdown to Portable Text

Convert Markdown strings into Portable Text blocks. Use this for importing content from Markdown-based systems (static site generators, GitHub READMEs, AI-generated content), processing user input, or migrating from Markdown-first CMSes.

Looking to render Portable Text as Markdown? See the Markdown rendering guide.

Terminal window
npm i @portabletext/markdown

markdownToPortableText takes a Markdown string and returns an array of Portable Text blocks.

import {markdownToPortableText} from '@portabletext/markdown'
const blocks = markdownToPortableText('# Hello **world**')

Standard Markdown elements (headings, paragraphs, bold, italic, links, lists, blockquotes, inline code) are handled automatically using the default schema.

The conversion is schema-driven. The library only outputs types that exist in the schema, so the output always matches your content model.

The default schema includes:

TypeValues
stylesnormal, h1-h6, blockquote
listsbullet, number
decoratorsstrong, em, code, strike-through
annotationslink (fields: href, title)
blockObjectscode, image, horizontal-rule, html, table, callout
inlineObjectsimage

To use a custom schema, import compileSchema and defineSchema from @portabletext/schema:

import {compileSchema, defineSchema} from '@portabletext/schema'
markdownToPortableText(markdown, {
schema: compileSchema(
defineSchema({
styles: [{name: 'normal'}, {name: 'heading 1'}],
}),
),
})

If you are using a Sanity schema, use @portabletext/sanity-bridge to convert it first:

import {sanitySchemaToPortableTextSchema} from '@portabletext/sanity-bridge'
const schema = sanitySchemaToPortableTextSchema(sanityBlockArraySchema)
markdownToPortableText(markdown, {schema})

Matchers control how Markdown elements map to schema types. The library includes defaults for all standard elements. You can override individual matchers when your schema uses different type names.

GroupMatcherMarkdownMaps to
blocknormalParagraphs'normal'
h1-h6#-###### headings'h1'-'h6'
blockquote> blockquotes'blockquote'
listItembullet- or * lists'bullet'
number1. ordered lists'number'
marksstrong**bold**'strong'
em*italic*'em'
code`inline code`'code'
strikeThrough~~strikethrough~~'strike-through'
link[text](url "title")'link'
typescodeFenced code blocks'code'
horizontalRule---'horizontal-rule'
image![alt](src)'image'
htmlHTML blocks'html'
callout> [!NOTE], etc.'callout'

Override a matcher when your schema uses a different name for a type. For example, if your schema uses 'heading 1' instead of 'h1':

markdownToPortableText(markdown, {
schema: compileSchema(
defineSchema({
/* your schema */
}),
),
block: {
h1: ({context}) => {
const style = context.schema.styles.find((s) => s.name === 'heading 1')
return style?.name
},
},
})

Returning undefined from a matcher skips the element gracefully. This is useful when a type may or may not exist in the schema depending on the content model.

FeatureMarkdown to PT
Headings (h1-h6)
Paragraphs
Bold
Italic
Inline code
Strikethrough
Links
Blockquotes
Ordered lists
Unordered lists
Nested lists
Code blocks
Horizontal rules
Images
Tables✅*
HTML blocks
Callouts✅*

* Requires custom schema configuration (see above).

Source formatTool
HTML → PT@portabletext/html
Gutenberg → PT@emdash-cms/gutenberg-to-portable-text (30+ block types)
Contentful → PT@portabletext/contentful-rich-text-to-portable-text