Skip to content
This page is available as Markdown at /conversion/markdown-to-portable-text.md. For the full documentation index, see /llms.txt, or the complete corpus at /llms-full.txt.

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:

| Type | Values | | --------------- | -------------------------------------------------------------- | | styles | normal, h1-h6, blockquote | | lists | bullet, number | | decorators | strong, em, code, strike-through | | annotations | link (fields: href, title) | | blockObjects | code, image, horizontal-rule, html, table, callout | | inlineObjects | image |

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.

| Group | Matcher | Markdown | Maps to | | ---------- | ---------------- | --------------------- | ------------------- | | block | normal | Paragraphs | 'normal' | | | h1-h6 | #-###### headings | 'h1'-'h6' | | | blockquote | > blockquotes | 'blockquote' | | listItem | bullet | - or * lists | 'bullet' | | | number | 1. ordered lists | 'number' | | marks | strong | **bold** | 'strong' | | | em | *italic* | 'em' | | | code | `inline code` | 'code' | | | strikeThrough | ~~strikethrough~~ | 'strike-through' | | | link | [text](url "title") | 'link' | | types | code | Fenced code blocks | 'code' | | | horizontalRule | --- | 'horizontal-rule' | | | image | ![alt](src) | 'image' | | | html | HTML 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.

| Feature | Markdown 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 format | Tool | | --------------- | ------------------------------------------------------------------------------------------------------------------------------ | | HTML → PT | @portabletext/html | | Gutenberg → PT | @emdash-cms/gutenberg-to-portable-text (30+ block types) | | Contentful → PT | @portabletext/contentful-rich-text-to-portable-text |