Portable Text Serialization
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 a unified approach to rendering Portable Text (PT) content using the @portabletext library family. It outlines the core component-mapping pattern used to transform PT blocks, marks, and custom types into framework-specific elements for React, Svelte, Vue, Astro, HTML, Markdown, and plain text. The documentation covers the structural anatomy of PT, best practices for component stability, and strategies for handling custom types and missing components.
What it does
- Maps Portable Text node types to framework-specific renderers.
- Provides a consistent component-mapping pattern for diverse frontend environments.
- Enables server-side conversion of PT to HTML or Markdown strings.
- Facilitates the extraction of plain text from complex PT structures.
- Offers guidance on handling custom block types like images or call-to-actions.
- Includes strategies for managing component stability and error handling during rendering.
When to use it
- When implementing Portable Text rendering in React, Svelte, Vue, or Astro applications.
- When you need to convert Portable Text to HTML or Markdown on the server.
- When building custom serializers for non-standard or custom block types.
- When you need to extract plain text from a Portable Text array.
- When troubleshooting rendering issues related to marks, lists, or nested block structures.
Advantages
- Provides a consistent API across multiple frontend frameworks.
- Simplifies the rendering of complex, nested content structures.
- Supports highly customizable output through explicit component mapping.
- Includes built-in mechanisms for handling missing components and debugging.
- Promotes performance by encouraging stable component definitions.
Usage examples
- “How do I render a custom image block in my React application using Portable Text?”
- You define a components object where the types key maps 'image' to a custom React component that receives the block data as a prop, then pass this object to the PortableText component.
- “How can I convert my Portable Text content to a plain text string?”
- You use the @portabletext/toolkit library to process the Portable Text array and extract the text content from the spans, ignoring formatting or block styles.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/sanity-io/portable-text-serialization/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip portable-text-serialization.zip -d .agents/skills
Documentation by sanity
Portable Text Serialization
Render Portable Text content across frameworks using the @portabletext/* library family. Each library follows the same component-mapping pattern: you provide a components object that maps PT node types to framework-specific renderers.
Portable Text Structure (Quick Reference)
PT is an array of blocks. Each block has _type, optional style, children (spans), markDefs, listItem, and level.
Root array
├── block (_type: "block")
│ ├── style: "normal" | "h1" | "h2" | "blockquote" | ...
│ ├── children: [span, span, ...]
│ │ └── span: { _type: "span", text: "...", marks: ["strong", "<markDefKey>"] }
│ ├── markDefs: [{ _key, _type: "link", href: "..." }, ...]
│ ├── listItem: "bullet" | "number" (optional)
│ └── level: 1, 2, 3... (optional, for nested lists)
├── custom block (_type: "image" | "code" | any custom type)
└── ...more blocksMarks come in two forms:
Decorators: string values in
marks[]like"strong","em","underline","code"Annotations: keys in
marks[]referencing entries inmarkDefs[](e.g., links, internal references)
Component Mapping Pattern (All Frameworks)
Every @portabletext/* library accepts a components object with these keys:
| Key | Renders | Props/Data |
|---|---|---|
types | Custom block/inline types (image, code, CTA) | value (the block data) |
marks | Decorators + annotations | children + value (mark data) |
block | Block styles (h1, normal, blockquote) | children |
list | List wrappers (ul, ol) | children |
listItem | List items | children |
hardBreak | Line breaks within a block | — |
Framework-Specific Rules
Read the rule file matching your framework:
React / Next.js:
rules/react.md—@portabletext/reactornext-sanitySvelte / SvelteKit:
rules/svelte.md—@portabletext/svelteVue / Nuxt:
rules/vue.md—@portabletext/vueAstro:
rules/astro.md—astro-portabletextHTML (server-side):
rules/html.md—@portabletext/to-htmlMarkdown:
rules/markdown.md—@portabletext/markdownPlain text extraction:
rules/plain-text.md—@portabletext/toolkit
Additional Community Serializers
These are listed on portabletext.org but don't have dedicated rule files:
| Target | Package |
|---|---|
| React Native | @portabletext/react-native-portabletext |
| React PDF | @portabletext/react-pdf-portabletext |
| Solid | solid-portabletext |
| Qwik | portabletext-qwik |
| Shopify Liquid | portable-text-to-liquid |
| PHP | sanity-php (SanityBlockContent class) |
| Python | portabletext-html |
| C# / .NET | dotnet-portable-text |
| Dart / Flutter | flutter_sanity_portable_text |
Common Patterns (All Frameworks)
Custom Types Need Explicit Components
PT renderers only handle standard blocks by default. Custom types (image, code, callToAction, etc.) require explicit component mappings — they won't render otherwise.
Keep Components Object Stable
In React/Vue, define components outside the render function or memoize it. Recreating on every render causes unnecessary re-renders.
Handle Missing Components Gracefully
All libraries accept onMissingComponent to control behavior when encountering unknown types:
false— suppress warningsCustom function — log or report
Querying PT with GROQ
Always expand references inside custom blocks:
body[]{
...,
_type == "image" => {
...,
asset->
},
markDefs[]{
...,
_type == "internalLink" => {
...,
"slug": @.reference->slug.current
}
}
}Files
- ATTRIBUTION.mdAdded by AutoGineer934 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md4.3 KiB
- rules/astro.md2.3 KiB
- rules/html.md3.1 KiB
- rules/markdown.md3.8 KiB
- rules/plain-text.md1.7 KiB
- rules/react.md3.5 KiB
- rules/svelte.md2.7 KiB
- rules/vue.md3.0 KiB
rules/markdown.md
title: Serialize Portable Text to Markdown description: Convert Portable Text to Markdown strings using @portabletext/markdown tags: [portable-text, markdown, serialization, conversion]
Serialize Portable Text to Markdown
Use @portabletext/markdown to convert PT blocks to Markdown strings. Useful for AI/LLM pipelines, static site generators, README generation, and anywhere Markdown is the target format.
npm install @portabletext/markdownBasic Usage
import {portableTextToMarkdown} from '@portabletext/markdown'
const markdown = portableTextToMarkdown(portableTextBlocks)Built-in Support
Out of the box, portableTextToMarkdown handles:
Headings (h1–h6)
Paragraphs
Bold (
**), italic (_), inline code (`), strikethrough (~~)Links (
[text](url))Blockquotes (
>)Ordered and unordered lists (including nested)
Code blocks (fenced with language)
Horizontal rules (
---)Images (
)Tables (GFM)
Built-in Type Renderers
The library exports default renderers for common block object types. Enable them explicitly:
import {
portableTextToMarkdown,
DefaultCodeBlockRenderer,
DefaultImageRenderer,
DefaultHorizontalRuleRenderer,
DefaultTableRenderer,
DefaultHtmlRenderer,
} from '@portabletext/markdown'
const markdown = portableTextToMarkdown(blocks, {
types: {
'code': DefaultCodeBlockRenderer, // {code, language?} → fenced code block
'image': DefaultImageRenderer, // {src, alt?, title?} → 
'horizontal-rule': DefaultHorizontalRuleRenderer, // → ---
'table': DefaultTableRenderer, // {rows, headerRows?} → GFM table
'html': DefaultHtmlRenderer, // {html} → raw HTML
},
})Custom Renderers
Handle custom block types and marks with renderer functions:
const markdown = portableTextToMarkdown(blocks, {
// Custom block types — receives {value, index, isInline}
types: {
callout: ({value}) => `> **${value.title}**\n> ${value.text}`,
image: ({value, isInline}) => {
if (isInline) return ''
return ``
},
},
// Custom block style renderers — receives {value, children, index}
block: {
h1: ({children}) => `# ${children}`,
blockquote: ({children}) => `> ${children}`,
},
// Custom mark renderers — receives {value, children, text, markType, markKey}
marks: {
highlight: ({children}) => `==${children}==`,
internalLink: ({children, value}) => `[${children}](/docs/${value.slug})`,
},
// Custom list item renderer — receives {value, children, listIndex}
listItem: ({children}) => children,
// Control spacing between blocks — function, not string
blockSpacing: ({current, next}) => {
if (current.listItem && next.listItem) return '\n'
return undefined // use default (\n\n)
},
// Handle unknown types gracefully
unknownType: ({value}) => `<!-- Unknown type: ${value._type} -->`,
unknownMark: ({children}) => children,
})Use Cases
| Use Case | Why Markdown |
|---|---|
| AI/LLM context | Models work well with Markdown input |
| Static site generators | Hugo, Jekyll, Eleventy consume Markdown |
| README generation | Generate docs from Sanity content |
| Email (with converter) | Markdown → HTML for email templates |
| Export/backup | Human-readable content export |
| Documentation pipelines | Sanity as docs CMS, output as Markdown |
Bidirectional: Also Converts Markdown → PT
The same package also provides markdownToPortableText() for the reverse direction. See the portable-text-conversion skill for details.
Reference
Part of the portabletext/editor monorepo
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.
Needs: Needs Node.js
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-serialization · 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-serialization.zip