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/react.md
title: Serialize Portable Text to React description: Render Portable Text in React and Next.js using @portabletext/react tags: [portable-text, react, nextjs, serialization, rendering]
Serialize Portable Text to React
Use @portabletext/react (or re-exported from next-sanity) to render PT in React/Next.js.
npm install @portabletext/reactBasic Usage
import {PortableText} from '@portabletext/react'
// or: import {PortableText} from 'next-sanity'
export function Body({value}: {value: PortableTextBlock[]}) {
return <PortableText value={value} components={components} />
}Typed Components Object
import type {PortableTextComponents} from '@portabletext/react'
const components: PortableTextComponents = {
// Block styles
block: {
h1: ({children}) => <h1 className="text-4xl font-bold">{children}</h1>,
h2: ({children}) => <h2 className="text-3xl font-semibold">{children}</h2>,
blockquote: ({children}) => (
<blockquote className="border-l-4 pl-4 italic">{children}</blockquote>
),
// 'normal' is the default paragraph style
},
// Custom block types
types: {
image: ({value}) => (
<img
src={urlFor(value).width(800).url()}
alt={value.alt || ''}
loading="lazy"
/>
),
code: ({value}) => (
<pre data-language={value.language}>
<code>{value.code}</code>
</pre>
),
},
// Marks (decorators + annotations)
marks: {
// Decorator
highlight: ({children}) => (
<span className="bg-yellow-200">{children}</span>
),
// Annotation
link: ({children, value}) => {
const rel = !value?.href?.startsWith('/') ? 'noreferrer noopener' : undefined
return (
<a href={value?.href} rel={rel}>
{children}
</a>
)
},
internalLink: ({children, value}) => (
<a href={`/${value?.slug}`}>{children}</a>
),
},
// Lists
list: {
bullet: ({children}) => <ul className="list-disc ml-6">{children}</ul>,
number: ({children}) => <ol className="list-decimal ml-6">{children}</ol>,
},
listItem: {
bullet: ({children}) => <li>{children}</li>,
},
}Props Reference
| Component type | Props received |
|---|---|
block.* | {children, value} — value is the full block |
types.* | {value, isInline} — value is the custom block data |
marks.* | {children, value, markType, markKey} — value is the markDef data |
list.* | {children, value} |
listItem.* | {children, value} |
Performance: Stabilize the Components Object
❌ Bad — recreated every render:
function Body({value}) {
return <PortableText value={value} components={{
types: {image: ({value}) => <img src={value.url} />}
}} />
}✅ Good — defined outside or memoized:
const components: PortableTextComponents = {
types: {image: ({value}) => <img src={value.url} />}
}
function Body({value}) {
return <PortableText value={value} components={components} />
}Plain Text Extraction
import {toPlainText} from '@portabletext/react'
const text = toPlainText(blocks) // for meta descriptions, search indexingTailwind Typography Shortcut
For simple blogs without custom blocks, wrap in prose:
<article className="prose lg:prose-xl">
<PortableText value={value} />
</article>Reference
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