portable-text-serialization — Claude Code skill for Portable Text
A skill that shows the agent how to render and serialize Portable Text to React, Svelte, Vue, Astro, HTML, Markdown and plain text.
npx skills add sanity-io/agent-toolkit --skill portable-text-serialization -a claude-codeWhat portable-text-serialization does
Portable Text is an array of blocks from Sanity: each block carries _type, a style, children with spans, markDefs, and for lists also listItem and level. The skill walks the agent through that shape. From there it points to the @portabletext/* family, where every library uses the same trick — you hand over a components object that maps node types to framework-specific renderers.
Separate rule files cover each target: rules/react.md for React and Next.js, rules/svelte.md, rules/vue.md, rules/astro.md, plus the server-side rules/html.md, rules/markdown.md and rules/plain-text.md. The components keys are the same everywhere: types for custom blocks like images or code, marks for decorators and annotations, then block, list, listItem and hardBreak.
Custom types are where this breaks. Renderers handle standard blocks out of the box, so an image, code or callToAction block stays invisible until you map it explicitly. In React and Vue, keep the components object outside the render function or memoize it — recreating it on every render causes needless re-renders. All the libraries accept onMissingComponent, so you can silence warnings for unknown types or route them to a log.
What it helps with
- render a Sanity article body in React or Next.js
- build an HTML string from Portable Text on the server
- convert Portable Text to Markdown
- pull plain text out of a block without markup
- write a serializer for a custom block such as an image or code
Example prompts for the agent
- Render this Portable Text in React and wire the markDefs links through next/link
- Convert the document to Markdown for an export
- Extract just the text from this block, no marks or links
How to install portable-text-serialization
1Skills CLI (Vercel)
npx skills add sanity-io/agent-toolkit --skill portable-text-serialization -a claude-code2Manually: put the files in place
~/.claude/skills/portable-text-serialization/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/portable-text-serialization/
1Skills CLI (Vercel)
npx skills add sanity-io/agent-toolkit --skill portable-text-serialization -a codex2Manually: put the files in place
.agents/skills/portable-text-serialization/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add sanity-io/agent-toolkit --skill portable-text-serialization -a cursor2Manually: put the files in place
.cursor/skills/portable-text-serialization/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install sanity-io/agent-toolkit portable-text-serialization2Manually: put the files in place
.github/skills/portable-text-serialization/SKILL.md1Skills CLI (Vercel)
npx skills add sanity-io/agent-toolkit --skill portable-text-serialization -a gemini-cli2Platform CLI
gemini skills install https://github.com/sanity-io/agent-toolkit --path skills/portable-text-serializationManually: put the files in place
.devin/skills/portable-text-serialization/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add sanity-io/agent-toolkit --skill portable-text-serialization -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about portable-text-serialization
How do I install portable-text-serialization in Claude?
Run npx skills add sanity-io/agent-toolkit --skill portable-text-serialization -a claude-code. The skill lands at ~/.claude/skills/portable-text-serialization/SKILL.md.
What does portable-text-serialization do?
It teaches the agent to render and serialize Portable Text: it reads the block, span and markDefs structure and picks the right @portabletext/* library for your framework.
Why do my custom blocks not show up?
Renderers only cover standard blocks. For image, code or your own types you have to map a component under the types key of the components object.
Source text
SKILL.md · MIT
---
name: portable-text-serialization
description: Render and serialize Portable Text to React, Svelte, Vue, Astro, HTML, Markdown, and plain text. Use when implementing Portable Text rendering in any frontend framework, building custom serializers for non-standard block types, converting Portable Text to HTML strings server-side, converting Portable Text to Markdown, extracting plain text from Portable Text, or troubleshooting rendering issues with marks, blocks, lists, or custom types.
license: MIT
metadata:
author: sanity
version: "1.0.0"
---
# 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 blocks
```
**Marks** come in two forms:
- **Decorators**: string values in `marks[]` like `"strong"`, `"em"`, `"underline"`, `"code"`
- **Annotations**: keys in `marks[]` referencing entries in `markDefs[]` (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/react` or `next-sanity`
- **Svelte / SvelteKit**: `rules/svelte.md` — `@portabletext/svelte`
- **Vue / Nuxt**: `rules/vue.md` — `@portabletext/vue`
- **Astro**: `rules/astro.md` — `astro-portabletext`
- **HTML (server-side)**: `rules/html.md` — `@portabletext/to-html`
- **Markdown**: `rules/markdown.md` — `@portabletext/markdown`
- **Plain text extraction**: `rules/plain-text.md` — `@portabletext/toolkit`
### Additional Community Serializers
These are listed on [portabletext.org](https://www.portabletext.org/integrations/serializers/) 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 warnings
- Custom function — log or report
### Querying PT with GROQ
Always expand references inside custom blocks:
```groq
body[]{
...,
_type == "image" => {
...,
asset->
},
markDefs[]{
...,
_type == "internalLink" => {
...,
"slug": @.reference->slug.current
}
}
}
```
© sanity · MIT · sanity-io/agent-toolkit