Portable Text Conversion
SanityOfficialCreated by sanity
- Skill
- Tool integration
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides methods and specifications for transforming external content formats like HTML and Markdown into the Portable Text format used by Sanity. It covers the use of specific libraries for automated conversion, as well as the structural requirements for manually constructing Portable Text blocks, ensuring developers can programmatically ingest or migrate content into their Sanity projects.
What it does
- Converts Markdown to Portable Text using the markdownToPortableText function.
- Parses HTML into Portable Text blocks using the htmlToBlocks utility.
- Provides the structural specification for manual Portable Text block construction.
- Explains the use of _key, _type, and markDefs within the Portable Text schema.
- Details how to handle lists and nested content structures.
When to use it
- When migrating content from a legacy CMS to Sanity.
- When building automated pipelines to ingest HTML or Markdown into a Sanity dataset.
- When programmatically generating rich text content from external APIs or databases.
- When you need to ensure custom content structures adhere to the Portable Text specification.
Advantages
- Standardizes content ingestion across different source formats.
- Provides clear paths for both automated and manual content conversion.
- Ensures compatibility with Sanity's internal data requirements.
- Reduces manual effort during large-scale content migrations.
Usage examples
- “How do I convert a Markdown string to Portable Text?”
- The agent provides the implementation steps using the markdownToPortableText function from the @portabletext/markdown library.
- “What is the structure of a Portable Text block?”
- The agent returns the JSON schema for a block, including required fields like _type, _key, and the children array.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/sanity-io/portable-text-conversion/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip portable-text-conversion.zip -d .agents/skills
Documentation by sanity
Portable Text Conversion
Convert external content (HTML, Markdown) into Portable Text for Sanity. Three main approaches:
markdownToPortableText— Convert Markdown directly using@portabletext/markdown(recommended for Markdown)htmlToBlocks— Parse HTML into PT blocks using@portabletext/block-tools(for HTML migration)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:
[
{
"_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_typemarkDefsholds annotation data;markson spans referencemarkDefs[*]._keyor are decorator stringsLists use
listItem("bullet" | "number") andlevel(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/markdownwithmarkdownToPortableText(recommended)HTML → Portable Text:
rules/html-to-pt.md—@portabletext/block-toolswithhtmlToBlocksManual PT Construction:
rules/manual-construction.md— build blocks programmatically from any source
Note:
@sanity/block-toolsis the legacy package name. Always use@portabletext/block-toolsfor new projects. The API is the same.
Files
- ATTRIBUTION.mdAdded by AutoGineer925 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md2.6 KiB
- rules/html-to-pt.md6.9 KiB
- rules/manual-construction.md4.9 KiB
- rules/markdown-to-pt.md5.5 KiB
rules/manual-construction.md
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:
import {randomKey} from '@sanity/util/content'
const key = randomKey(12) // e.g., "a1b2c3d4e5f6"Or use a simple helper:
const randomKey = () => Math.random().toString(36).slice(2, 14)Building Blocks
Simple Paragraph
{
_type: 'block',
_key: randomKey(),
style: 'normal',
children: [
{_type: 'span', _key: randomKey(), text: 'Hello world', marks: []}
],
markDefs: []
}Heading
{
_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)
{
_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:
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):
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:
// 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:
// 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:
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
_typeand_key[ ] Every span has
_type: "span",_key,text, andmarks[ ] Every annotation key in
marks[]has a matching entry inmarkDefs[][ ]
markDefsentries have_typeand_key[ ]
_keyvalues are unique within the array[ ]
stylevalues match your schema's allowed styles[ ]
listItemvalues match your schema's allowed list types[ ] Custom block
_typevalues match registered schema types
Reference
@sanity/util — provides
randomKey()for generating_keyvalues
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- sanity
- Publisher
- Sanity (Official)
- Source
- sanity-io/agent-toolkit · skills/portable-text-conversion · commit 3ed3a14
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by sanity.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download portable-text-conversion.zip