This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
---
|
||||
name: portable-text-conversion
|
||||
description: 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.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: sanity
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Portable Text Conversion
|
||||
|
||||
Convert external content (HTML, Markdown) into Portable Text for Sanity. Three main approaches:
|
||||
|
||||
1. **`markdownToPortableText`** — Convert Markdown directly using `@portabletext/markdown` (recommended for Markdown)
|
||||
2. **`htmlToBlocks`** — Parse HTML into PT blocks using `@portabletext/block-tools` (for HTML migration)
|
||||
3. **Manual construction** — Build PT blocks directly from any source (APIs, databases, etc.)
|
||||
|
||||
## Portable Text Specification
|
||||
|
||||
Understand the target format before converting. PT is an array of blocks:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"_type": "block",
|
||||
"_key": "abc123",
|
||||
"style": "normal",
|
||||
"children": [
|
||||
{"_type": "span", "_key": "def456", "text": "Hello ", "marks": []},
|
||||
{"_type": "span", "_key": "ghi789", "text": "world", "marks": ["strong"]}
|
||||
],
|
||||
"markDefs": []
|
||||
},
|
||||
{
|
||||
"_type": "block",
|
||||
"_key": "jkl012",
|
||||
"style": "h2",
|
||||
"children": [
|
||||
{"_type": "span", "_key": "mno345", "text": "A heading", "marks": []}
|
||||
],
|
||||
"markDefs": []
|
||||
},
|
||||
{
|
||||
"_type": "image",
|
||||
"_key": "pqr678",
|
||||
"asset": {"_type": "reference", "_ref": "image-abc-200x200-png"}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Key rules:**
|
||||
- Every block and span needs `_key` (unique within the array)
|
||||
- `_type: "block"` is for text blocks; custom types use their own `_type`
|
||||
- `markDefs` holds annotation data; `marks` on spans reference `markDefs[*]._key` or are decorator strings
|
||||
- Lists use `listItem` ("bullet" | "number") and `level` (1, 2, 3...) on regular blocks
|
||||
|
||||
## Conversion Rules
|
||||
|
||||
Read the rule file matching your source format:
|
||||
|
||||
- **Markdown → Portable Text**: `rules/markdown-to-pt.md` — `@portabletext/markdown` with `markdownToPortableText` (recommended)
|
||||
- **HTML → Portable Text**: `rules/html-to-pt.md` — `@portabletext/block-tools` with `htmlToBlocks`
|
||||
- **Manual PT Construction**: `rules/manual-construction.md` — build blocks programmatically from any source
|
||||
|
||||
> **Note:** `@sanity/block-tools` is the legacy package name. Always use `@portabletext/block-tools` for new projects. The API is the same.
|
||||
@@ -0,0 +1,242 @@
|
||||
---
|
||||
title: Convert HTML to Portable Text
|
||||
description: Use @portabletext/block-tools with htmlToBlocks to convert HTML content into Portable Text blocks
|
||||
tags: [portable-text, html, conversion, migration, import]
|
||||
---
|
||||
|
||||
# Convert HTML to Portable Text
|
||||
|
||||
Use `@portabletext/block-tools` to parse HTML into Portable Text blocks. This is the primary tool for migrating HTML content from legacy CMSs. It has built-in support for content from Google Docs, Microsoft Word, and Notion.
|
||||
|
||||
> **Note:** For Markdown sources, use `@portabletext/markdown` instead — it's simpler and more direct. See `rules/markdown-to-pt.md`.
|
||||
|
||||
> **Note:** `@sanity/block-tools` is the legacy package name. Use `@portabletext/block-tools` for new projects. The API is identical.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
npm install @portabletext/block-tools jsdom @sanity/schema
|
||||
```
|
||||
|
||||
In Node.js, you must provide a `parseHtml` function that returns a DOM `Document`. Use JSDOM for this:
|
||||
|
||||
```ts
|
||||
import {htmlToBlocks} from '@portabletext/block-tools'
|
||||
import {JSDOM} from 'jsdom'
|
||||
import Schema from '@sanity/schema'
|
||||
|
||||
// JSDOM is passed to htmlToBlocks via the parseHtml option:
|
||||
// htmlToBlocks(html, blockContentType, {
|
||||
// parseHtml: (html) => new JSDOM(html).window.document,
|
||||
// })
|
||||
```
|
||||
|
||||
## Define Your Schema
|
||||
|
||||
`htmlToBlocks` needs a compiled Sanity block content type to know which marks, styles, and custom types are valid. Use `@sanity/schema` to compile it:
|
||||
|
||||
```ts
|
||||
const defaultSchema = Schema.compile({
|
||||
name: 'mySchema',
|
||||
types: [
|
||||
{
|
||||
name: 'post',
|
||||
type: 'document',
|
||||
fields: [
|
||||
{
|
||||
name: 'body',
|
||||
type: 'array',
|
||||
of: [
|
||||
{
|
||||
type: 'block',
|
||||
marks: {
|
||||
decorators: [
|
||||
{title: 'Strong', value: 'strong'},
|
||||
{title: 'Emphasis', value: 'em'},
|
||||
{title: 'Code', value: 'code'},
|
||||
],
|
||||
annotations: [
|
||||
{
|
||||
name: 'link',
|
||||
type: 'object',
|
||||
fields: [{name: 'href', type: 'url'}],
|
||||
},
|
||||
],
|
||||
},
|
||||
styles: [
|
||||
{title: 'Normal', value: 'normal'},
|
||||
{title: 'H2', value: 'h2'},
|
||||
{title: 'H3', value: 'h3'},
|
||||
{title: 'Quote', value: 'blockquote'},
|
||||
],
|
||||
lists: [
|
||||
{title: 'Bullet', value: 'bullet'},
|
||||
{title: 'Number', value: 'number'},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
type: 'image',
|
||||
fields: [{name: 'alt', type: 'string'}],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const blockContentType = defaultSchema
|
||||
.get('post')
|
||||
.fields.find((f) => f.name === 'body').type
|
||||
```
|
||||
|
||||
## Basic Conversion
|
||||
|
||||
```ts
|
||||
const html = '<p>Hello <strong>world</strong></p><h2>Heading</h2>'
|
||||
|
||||
const blocks = htmlToBlocks(html, blockContentType, {
|
||||
parseHtml: (html) => new JSDOM(html).window.document,
|
||||
})
|
||||
```
|
||||
|
||||
## Custom Deserializers
|
||||
|
||||
Handle HTML elements that don't map directly to standard PT:
|
||||
|
||||
```ts
|
||||
const blocks = htmlToBlocks(html, blockContentType, {
|
||||
parseHtml: (html) => new JSDOM(html).window.document,
|
||||
rules: [
|
||||
// Convert <img> to image blocks
|
||||
{
|
||||
deserialize(el, next, block) {
|
||||
if (el.tagName?.toLowerCase() !== 'img') return undefined
|
||||
|
||||
return block({
|
||||
_type: 'image',
|
||||
asset: {
|
||||
_type: 'reference',
|
||||
_ref: '', // Upload image separately, set ref after
|
||||
},
|
||||
alt: el.getAttribute('alt') || '',
|
||||
_sanityAsset: `image@${el.getAttribute('src')}`, // for migration tooling
|
||||
})
|
||||
},
|
||||
},
|
||||
// Convert <a> with custom attributes
|
||||
{
|
||||
deserialize(el, next, block) {
|
||||
if (el.tagName?.toLowerCase() !== 'a') return undefined
|
||||
|
||||
const href = el.getAttribute('href') || ''
|
||||
const target = el.getAttribute('target') || ''
|
||||
|
||||
return {
|
||||
_type: '__annotation',
|
||||
markDef: {
|
||||
_type: 'link',
|
||||
href,
|
||||
...(target ? {target} : {}),
|
||||
},
|
||||
children: next(el.childNodes),
|
||||
}
|
||||
},
|
||||
},
|
||||
// Convert <iframe> to embed blocks
|
||||
{
|
||||
deserialize(el, next, block) {
|
||||
if (el.tagName?.toLowerCase() !== 'iframe') return undefined
|
||||
|
||||
return block({
|
||||
_type: 'embed',
|
||||
url: el.getAttribute('src') || '',
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
## Pre-Process HTML Before Conversion
|
||||
|
||||
Strip layout elements and extract metadata:
|
||||
|
||||
```ts
|
||||
function preprocessHtml(rawHtml: string) {
|
||||
const dom = new JSDOM(rawHtml)
|
||||
const doc = dom.window.document
|
||||
|
||||
// Remove layout elements
|
||||
const removeSelectors = ['header', 'footer', 'nav', '.sidebar', '.menu', 'script', 'style']
|
||||
removeSelectors.forEach((sel) => {
|
||||
doc.querySelectorAll(sel).forEach((el) => el.remove())
|
||||
})
|
||||
|
||||
// Extract metadata
|
||||
const title = doc.querySelector('h1')?.textContent || doc.title || ''
|
||||
const description = doc.querySelector('meta[name="description"]')?.getAttribute('content') || ''
|
||||
|
||||
// Get cleaned body
|
||||
const body = doc.querySelector('article')?.innerHTML || doc.body.innerHTML
|
||||
|
||||
return {title, description, body}
|
||||
}
|
||||
```
|
||||
|
||||
## Upload Images During Migration
|
||||
|
||||
Don't just link external images — upload them to Sanity:
|
||||
|
||||
```ts
|
||||
import type {SanityClient} from '@sanity/client'
|
||||
|
||||
async function uploadImage(client: SanityClient, url: string) {
|
||||
const response = await fetch(url)
|
||||
const buffer = await response.arrayBuffer()
|
||||
const asset = await client.assets.upload('image', Buffer.from(buffer), {
|
||||
filename: url.split('/').pop(),
|
||||
})
|
||||
return {
|
||||
_type: 'image',
|
||||
asset: {_type: 'reference', _ref: asset._id},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Full Migration Example
|
||||
|
||||
```ts
|
||||
import {defineMigration, createOrReplace} from 'sanity/migrate'
|
||||
|
||||
export default defineMigration({
|
||||
title: 'Import WordPress posts',
|
||||
async *migrate(documents, context) {
|
||||
const posts = await fetchWordPressPosts()
|
||||
|
||||
for (const post of posts) {
|
||||
const {title, description, body} = preprocessHtml(post.content)
|
||||
const blocks = htmlToBlocks(body, blockContentType, {
|
||||
parseHtml: (html) => new JSDOM(html).window.document,
|
||||
rules: [/* custom rules */],
|
||||
})
|
||||
|
||||
yield createOrReplace({
|
||||
_id: `post-${post.slug}`,
|
||||
_type: 'post',
|
||||
title: title || post.title,
|
||||
body: blocks,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Run with: `sanity migration run import-wordpress-posts --no-dry-run`
|
||||
|
||||
## Reference
|
||||
|
||||
- [@portabletext/block-tools](https://github.com/portabletext/editor/tree/main/packages/block-tools) — part of the `portabletext/editor` monorepo
|
||||
- [Sanity Migration docs](https://www.sanity.io/docs/schema-and-content-migrations)
|
||||
- [portabletext.org](https://www.portabletext.org) — Editor docs and serializer list
|
||||
@@ -0,0 +1,210 @@
|
||||
---
|
||||
title: Manually Construct Portable Text Blocks
|
||||
description: Build Portable Text blocks programmatically from any data source
|
||||
tags: [portable-text, construction, api, programmatic, migration]
|
||||
---
|
||||
|
||||
# Manually Construct Portable Text Blocks
|
||||
|
||||
Build PT blocks directly when converting from non-HTML sources (APIs, databases, custom formats) or when you need precise control over the output.
|
||||
|
||||
## Key Generation
|
||||
|
||||
Every block, span, and markDef needs a unique `_key`:
|
||||
|
||||
```ts
|
||||
import {randomKey} from '@sanity/util/content'
|
||||
|
||||
const key = randomKey(12) // e.g., "a1b2c3d4e5f6"
|
||||
```
|
||||
|
||||
Or use a simple helper:
|
||||
|
||||
```ts
|
||||
const randomKey = () => Math.random().toString(36).slice(2, 14)
|
||||
```
|
||||
|
||||
## Building Blocks
|
||||
|
||||
### Simple Paragraph
|
||||
|
||||
```ts
|
||||
{
|
||||
_type: 'block',
|
||||
_key: randomKey(),
|
||||
style: 'normal',
|
||||
children: [
|
||||
{_type: 'span', _key: randomKey(), text: 'Hello world', marks: []}
|
||||
],
|
||||
markDefs: []
|
||||
}
|
||||
```
|
||||
|
||||
### Heading
|
||||
|
||||
```ts
|
||||
{
|
||||
_type: 'block',
|
||||
_key: randomKey(),
|
||||
style: 'h2', // h1, h2, h3, h4, h5, h6
|
||||
children: [
|
||||
{_type: 'span', _key: randomKey(), text: 'Section Title', marks: []}
|
||||
],
|
||||
markDefs: []
|
||||
}
|
||||
```
|
||||
|
||||
### Text with Decorators (Bold, Italic, Code)
|
||||
|
||||
```ts
|
||||
{
|
||||
_type: 'block',
|
||||
_key: randomKey(),
|
||||
style: 'normal',
|
||||
children: [
|
||||
{_type: 'span', _key: randomKey(), text: 'This is ', marks: []},
|
||||
{_type: 'span', _key: randomKey(), text: 'bold', marks: ['strong']},
|
||||
{_type: 'span', _key: randomKey(), text: ' and ', marks: []},
|
||||
{_type: 'span', _key: randomKey(), text: 'italic', marks: ['em']},
|
||||
{_type: 'span', _key: randomKey(), text: ' text.', marks: []},
|
||||
],
|
||||
markDefs: []
|
||||
}
|
||||
```
|
||||
|
||||
### Text with Annotations (Links)
|
||||
|
||||
Annotations require a `markDef` entry and a matching key in `marks`:
|
||||
|
||||
```ts
|
||||
const linkKey = randomKey()
|
||||
|
||||
{
|
||||
_type: 'block',
|
||||
_key: randomKey(),
|
||||
style: 'normal',
|
||||
children: [
|
||||
{_type: 'span', _key: randomKey(), text: 'Visit ', marks: []},
|
||||
{_type: 'span', _key: randomKey(), text: 'Sanity', marks: [linkKey]},
|
||||
{_type: 'span', _key: randomKey(), text: ' for more.', marks: []},
|
||||
],
|
||||
markDefs: [
|
||||
{_type: 'link', _key: linkKey, href: 'https://www.sanity.io'}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Overlapping Marks
|
||||
|
||||
A span can have multiple marks (both decorators and annotations):
|
||||
|
||||
```ts
|
||||
const linkKey = randomKey()
|
||||
|
||||
// "bold link" — both strong and linked
|
||||
{_type: 'span', _key: randomKey(), text: 'bold link', marks: ['strong', linkKey]}
|
||||
```
|
||||
|
||||
### Lists
|
||||
|
||||
Lists are regular blocks with `listItem` and `level`:
|
||||
|
||||
```ts
|
||||
// Bullet list
|
||||
[
|
||||
{
|
||||
_type: 'block', _key: randomKey(), style: 'normal',
|
||||
listItem: 'bullet', level: 1,
|
||||
children: [{_type: 'span', _key: randomKey(), text: 'First item', marks: []}],
|
||||
markDefs: []
|
||||
},
|
||||
{
|
||||
_type: 'block', _key: randomKey(), style: 'normal',
|
||||
listItem: 'bullet', level: 1,
|
||||
children: [{_type: 'span', _key: randomKey(), text: 'Second item', marks: []}],
|
||||
markDefs: []
|
||||
},
|
||||
{
|
||||
_type: 'block', _key: randomKey(), style: 'normal',
|
||||
listItem: 'bullet', level: 2, // nested
|
||||
children: [{_type: 'span', _key: randomKey(), text: 'Nested item', marks: []}],
|
||||
markDefs: []
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
### Custom Block Types
|
||||
|
||||
Any object with `_type` and `_key` can be a block:
|
||||
|
||||
```ts
|
||||
// Image block
|
||||
{
|
||||
_type: 'image',
|
||||
_key: randomKey(),
|
||||
asset: {_type: 'reference', _ref: 'image-abc123-800x600-png'},
|
||||
alt: 'A description',
|
||||
}
|
||||
|
||||
// Code block
|
||||
{
|
||||
_type: 'code',
|
||||
_key: randomKey(),
|
||||
language: 'typescript',
|
||||
code: 'const x = 42',
|
||||
}
|
||||
|
||||
// YouTube embed
|
||||
{
|
||||
_type: 'youtube',
|
||||
_key: randomKey(),
|
||||
url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||
}
|
||||
```
|
||||
|
||||
## Helper Function
|
||||
|
||||
A utility for building common blocks:
|
||||
|
||||
```ts
|
||||
function createBlock(
|
||||
text: string,
|
||||
style: string = 'normal',
|
||||
options?: {listItem?: string; level?: number}
|
||||
) {
|
||||
return {
|
||||
_type: 'block',
|
||||
_key: randomKey(),
|
||||
style,
|
||||
...(options?.listItem ? {listItem: options.listItem, level: options.level || 1} : {}),
|
||||
children: [{_type: 'span', _key: randomKey(), text, marks: []}],
|
||||
markDefs: [],
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const blocks = [
|
||||
createBlock('Introduction', 'h2'),
|
||||
createBlock('This is a paragraph.'),
|
||||
createBlock('First point', 'normal', {listItem: 'bullet', level: 1}),
|
||||
createBlock('Second point', 'normal', {listItem: 'bullet', level: 1}),
|
||||
]
|
||||
```
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
Before writing PT blocks to Sanity, verify:
|
||||
|
||||
- [ ] Every block has `_type` and `_key`
|
||||
- [ ] Every span has `_type: "span"`, `_key`, `text`, and `marks`
|
||||
- [ ] Every annotation key in `marks[]` has a matching entry in `markDefs[]`
|
||||
- [ ] `markDefs` entries have `_type` and `_key`
|
||||
- [ ] `_key` values are unique within the array
|
||||
- [ ] `style` values match your schema's allowed styles
|
||||
- [ ] `listItem` values match your schema's allowed list types
|
||||
- [ ] Custom block `_type` values match registered schema types
|
||||
|
||||
## Reference
|
||||
|
||||
- [Portable Text Specification](https://github.com/portabletext/portabletext)
|
||||
- [@sanity/util](https://github.com/sanity-io/sanity/tree/next/packages/%40sanity/util) — provides `randomKey()` for generating `_key` values
|
||||
@@ -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