Files
theater-ziefen-website/.agents/skills/portable-text-conversion/rules/markdown-to-pt.md
T
johannes.gasser 57af0b8386
Build and Deploy / build-and-deploy (push) Successful in 2m53s
add skills
2026-05-18 08:39:42 +02:00

5.5 KiB
Raw Blame History

title, description, tags
title description tags
Convert Markdown to Portable Text Convert Markdown content into Portable Text blocks using @portabletext/markdown
portable-text
markdown
conversion
migration
import

Convert Markdown to Portable Text

Use @portabletext/markdown for direct Markdown ↔ Portable Text conversion. This is the official library, part of the portabletext/editor monorepo.

npm install @portabletext/markdown

Basic Usage

import {markdownToPortableText} from '@portabletext/markdown'

const blocks = markdownToPortableText('# Hello **world**')

Output:

[{
  "_type": "block",
  "_key": "f4s8k2",
  "style": "h1",
  "children": [
    {"_type": "span", "_key": "a9c3x1", "text": "Hello ", "marks": []},
    {"_type": "span", "_key": "b7d2m5", "text": "world", "marks": ["strong"]}
  ],
  "markDefs": []
}]

Supported Markdown Features

Out of the box:

  • Headings (h1h6)
  • Paragraphs
  • Bold, italic, inline code, strikethrough
  • Links
  • Blockquotes
  • Ordered and unordered lists (including nested)
  • Code blocks (fenced with language)
  • Horizontal rules
  • Images
  • Tables (GFM)
  • HTML blocks (configurable)

Custom Schema Mapping

Control how Markdown elements map to your PT schema. Define a schema with @portabletext/schema:

import {markdownToPortableText} from '@portabletext/markdown'
import {defineSchema, compileSchema} from '@portabletext/schema'

const schema = compileSchema(defineSchema({
  styles: [{name: 'normal'}, {name: 'heading 1'}, {name: 'heading 2'}],
  decorators: [{name: 'strong'}, {name: 'em'}],
  annotations: [{name: 'link'}],
  lists: [{name: 'bullet'}, {name: 'number'}],
}))

const blocks = markdownToPortableText(markdown, {
  schema,
  // Map Markdown heading levels to custom style names
  block: {
    h1: ({context}) => 'heading 1',
    h2: ({context}) => 'heading 2',
  },
})

Using a Sanity Studio Schema

Use @portabletext/sanity-bridge to convert your Sanity block array schema:

import {markdownToPortableText} from '@portabletext/markdown'
import {sanitySchemaToPortableTextSchema} from '@portabletext/sanity-bridge'

// Convert a Sanity block array schema to a Portable Text schema
const schema = sanitySchemaToPortableTextSchema(sanityBlockArraySchema)

const blocks = markdownToPortableText(markdown, {schema})

Custom Matchers

Matchers are top-level options (not nested under a matchers key). Each receives {context, value} where context.schema lets you validate against the schema:

const blocks = markdownToPortableText(markdown, {
  // Block matchers — map Markdown block elements to PT styles
  block: {
    h1: ({context}) => {
      const style = context.schema.styles.find((s) => s.name === 'heading 1')
      return style?.name // Return undefined to skip
    },
  },
  // Mark matchers — map Markdown inline elements to PT marks
  marks: {
    strong: ({context}) => 'strong',
  },
  // Type matchers — map Markdown elements to custom PT block types
  types: {
    table: ({context, value}) => {
      const tableType = context.schema.blockObjects.find((obj) => obj.name === 'table')
      if (!tableType) return undefined
      return {
        _type: 'table',
        _key: context.keyGenerator(),
        rows: value.rows,
        headerRows: value.headerRows,
      }
    },
  },
})

Handling Inline HTML

Configure how inline HTML in Markdown is processed:

const blocks = markdownToPortableText(markdown, {
  html: {
    inline: 'text', // 'text' preserves as text, 'skip' removes
  },
})

Custom Key Generation

Provide your own key generator:

import {randomKey} from '@sanity/util/content'

const blocks = markdownToPortableText(markdown, {
  keyGenerator: () => randomKey(12),
})

Bidirectional: Also Converts PT → Markdown

The same package provides portableTextToMarkdown():

import {portableTextToMarkdown} from '@portabletext/markdown'

const markdown = portableTextToMarkdown(blocks)

See the portable-text-serialization skill's rules/markdown.md for details on PT → Markdown.

Migration Example

import {markdownToPortableText} from '@portabletext/markdown'
import {createClient} from '@sanity/client'
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'

const client = createClient({projectId: 'xxx', dataset: 'production', token: '...'})

// Import a directory of Markdown files
const mdFiles = fs.readdirSync('./content').filter(f => f.endsWith('.md'))

for (const file of mdFiles) {
  const raw = fs.readFileSync(path.join('./content', file), 'utf-8')
  const {data: frontmatter, content} = matter(raw)

  const body = markdownToPortableText(content)

  await client.createOrReplace({
    _id: `post-${path.basename(file, '.md')}`,
    _type: 'post',
    title: frontmatter.title,
    body,
  })
}

When to Use htmlToBlocks Instead

Use @portabletext/block-tools (htmlToBlocks) when:

  • Your source is HTML, not Markdown
  • You need custom deserializer rules for non-standard HTML elements
  • You're migrating from a CMS that exports HTML (WordPress, Contentful, etc.)
  • You need to handle complex HTML structures (tables with merged cells, nested divs, etc.)

For Markdown sources, @portabletext/markdown is simpler and more direct.

Reference