This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
---
|
||||
title: Convert Markdown to Portable Text
|
||||
description: Convert Markdown content into Portable Text blocks using @portabletext/markdown
|
||||
tags: [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.
|
||||
|
||||
```bash
|
||||
npm install @portabletext/markdown
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```ts
|
||||
import {markdownToPortableText} from '@portabletext/markdown'
|
||||
|
||||
const blocks = markdownToPortableText('# Hello **world**')
|
||||
```
|
||||
|
||||
Output:
|
||||
```json
|
||||
[{
|
||||
"_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`:
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```ts
|
||||
const blocks = markdownToPortableText(markdown, {
|
||||
html: {
|
||||
inline: 'text', // 'text' preserves as text, 'skip' removes
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Custom Key Generation
|
||||
|
||||
Provide your own key generator:
|
||||
|
||||
```ts
|
||||
import {randomKey} from '@sanity/util/content'
|
||||
|
||||
const blocks = markdownToPortableText(markdown, {
|
||||
keyGenerator: () => randomKey(12),
|
||||
})
|
||||
```
|
||||
|
||||
## Bidirectional: Also Converts PT → Markdown
|
||||
|
||||
The same package provides `portableTextToMarkdown()`:
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
- [@portabletext/markdown](https://github.com/portabletext/editor/tree/main/packages/markdown)
|
||||
- Part of the [portabletext/editor](https://github.com/portabletext/editor) monorepo
|
||||
- Uses [markdown-it](https://github.com/markdown-it/markdown-it) internally
|
||||
Reference in New Issue
Block a user