All skills
sanity-io avatar

/portable-text-conversion

@a11c399 official
by Sanitysanity-io/agent-toolkit187 stars
30

Convert HTML and Markdown content into Portable Text blocks for Sanity. Use when migrating content from legacy CMSs, importing HTML or Markdown into Sanity, building content pipelines that ingest external content, converting rich text between formats, or programmatically creating Portable Text documents. Covers @portabletext/markdown (markdownToPortableText), @portabletext/block-tools (htmlToBlocks), custom deserializers, and the Portable Text specification for manual block construction.

Use this Skill: https://skilld.dev/gh/sanity-io/agent-toolkit/portable-text-conversion

This session only. Nothing lands on disk.

rulesmarkdown-to-pt.md

≈1.4k tokens on demand. Your agent reads this file only when SKILL.md points to it.

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 (h1–h6)
  • 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

Source: SKILL.md on GitHub

No alerts16d4 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    This skill provides documentation and code examples for converting HTML and Markdown content into Sanity's Portable Text format using official and well-known libraries.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at a11c399. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated 8 months ago
metadata
{
  "author": "sanity",
  "version": "1.0.0"
}
  • sanity
  • portable-text
  • html
  • markdown
  • content-migration
  • cms
  • portabletext
  • block-tools

README badge

README badge for sanity-io/agent-toolkit/portable-text-conversion

Converts HTML and Markdown content into Portable Text blocks for Sanity using @portabletext/markdown and @portabletext/block-tools. Useful for migrating content from legacy CMSs, importing external rich text into Sanity, or programmatically building Portable Text documents from various sources.

Generated from the current SKILL.md.

Does this skill work with HTML and Markdown, or just one format?
It covers both. Use @portabletext/markdown for Markdown sources, @portabletext/block-tools for HTML sources, or manually construct blocks from any other format.
What is Portable Text?
Portable Text is Sanity's JSON-based rich text format. It represents content as an array of typed blocks (text blocks, images, custom types) with annotations, marks, and nested structures.
Can I convert from APIs or databases, not just HTML/Markdown?
Yes. The skill includes manual block construction rules for building Portable Text blocks programmatically from any data source.
Is @sanity/block-tools the correct package to use?
No. Use @portabletext/block-tools instead; @sanity/block-tools is the legacy package name. The API is the same.

Generated from the current SKILL.md. These answers refresh after source changes.