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.
Install
Section titled “Install”npm i @portabletext/markdownpnpm add @portabletext/markdownyarn add @portabletext/markdownBasic usage
Section titled “Basic usage”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.
Schema configuration
Section titled “Schema configuration”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
Section titled “Matchers”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 |  | '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.
Supported features
Section titled “Supported features”| 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).
Other conversion paths
Section titled “Other conversion paths”| 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 |
Further reading
Section titled “Further reading”- Markdown rendering guide for converting PT blocks to Markdown strings
@portabletext/markdownon GitHub for full API documentation and changelog