$npx -y skills add sanity-io/agent-toolkit --skill portable-text-serializationRender 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
| 1 | # Portable Text Serialization |
| 2 | |
| 3 | 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. |
| 4 | |
| 5 | ## Portable Text Structure (Quick Reference) |
| 6 | |
| 7 | PT is an array of blocks. Each block has `_type`, optional `style`, `children` (spans), `markDefs`, `listItem`, and `level`. |
| 8 | |
| 9 | ``` |
| 10 | Root array |
| 11 | ├── block (_type: "block") |
| 12 | │ ├── style: "normal" | "h1" | "h2" | "blockquote" | ... |
| 13 | │ ├── children: [span, span, ...] |
| 14 | │ │ └── span: { _type: "span", text: "...", marks: ["strong", "<markDefKey>"] } |
| 15 | │ ├── markDefs: [{ _key, _type: "link", href: "..." }, ...] |
| 16 | │ ├── listItem: "bullet" | "number" (optional) |
| 17 | │ └── level: 1, 2, 3... (optional, for nested lists) |
| 18 | ├── custom block (_type: "image" | "code" | any custom type) |
| 19 | └── ...more blocks |
| 20 | ``` |
| 21 | |
| 22 | **Marks** come in two forms: |
| 23 | - **Decorators**: string values in `marks[]` like `"strong"`, `"em"`, `"underline"`, `"code"` |
| 24 | - **Annotations**: keys in `marks[]` referencing entries in `markDefs[]` (e.g., links, internal references) |
| 25 | |
| 26 | ## Component Mapping Pattern (All Frameworks) |
| 27 | |
| 28 | Every `@portabletext/*` library accepts a `components` object with these keys: |
| 29 | |
| 30 | | Key | Renders | Props/Data | |
| 31 | |-----|---------|------------| |
| 32 | | `types` | Custom block/inline types (image, code, CTA) | `value` (the block data) | |
| 33 | | `marks` | Decorators + annotations | `children` + `value` (mark data) | |
| 34 | | `block` | Block styles (h1, normal, blockquote) | `children` | |
| 35 | | `list` | List wrappers (ul, ol) | `children` | |
| 36 | | `listItem` | List items | `children` | |
| 37 | | `hardBreak` | Line breaks within a block | — | |
| 38 | |
| 39 | ## Framework-Specific Rules |
| 40 | |
| 41 | Read the rule file matching your framework: |
| 42 | |
| 43 | - **React / Next.js**: `rules/react.md` — `@portabletext/react` or `next-sanity` |
| 44 | - **Svelte / SvelteKit**: `rules/svelte.md` — `@portabletext/svelte` |
| 45 | - **Vue / Nuxt**: `rules/vue.md` — `@portabletext/vue` |
| 46 | - **Astro**: `rules/astro.md` — `astro-portabletext` |
| 47 | - **HTML (server-side)**: `rules/html.md` — `@portabletext/to-html` |
| 48 | - **Markdown**: `rules/markdown.md` — `@portabletext/markdown` |
| 49 | - **Plain text extraction**: `rules/plain-text.md` — `@portabletext/toolkit` |
| 50 | |
| 51 | ### Additional Community Serializers |
| 52 | |
| 53 | These are listed on [portabletext.org](https://www.portabletext.org/integrations/serializers/) but don't have dedicated rule files: |
| 54 | |
| 55 | | Target | Package | |
| 56 | |--------|---------| |
| 57 | | React Native | `@portabletext/react-native-portabletext` | |
| 58 | | React PDF | `@portabletext/react-pdf-portabletext` | |
| 59 | | Solid | `solid-portabletext` | |
| 60 | | Qwik | `portabletext-qwik` | |
| 61 | | Shopify Liquid | `portable-text-to-liquid` | |
| 62 | | PHP | `sanity-php` (SanityBlockContent class) | |
| 63 | | Python | `portabletext-html` | |
| 64 | | C# / .NET | `dotnet-portable-text` | |
| 65 | | Dart / Flutter | `flutter_sanity_portable_text` | |
| 66 | |
| 67 | ## Common Patterns (All Frameworks) |
| 68 | |
| 69 | ### Custom Types Need Explicit Components |
| 70 | |
| 71 | PT renderers only handle standard blocks by default. Custom types (`image`, `code`, `callToAction`, etc.) require explicit component mappings — they won't render otherwise. |
| 72 | |
| 73 | ### Keep Components Object Stable |
| 74 | |
| 75 | In React/Vue, define `components` outside the render function or memoize it. Recreating on every render causes unnecessary re-renders. |
| 76 | |
| 77 | ### Handle Missing Components Gracefully |
| 78 | |
| 79 | All libraries accept `onMissingComponent` to control behavior when encountering unknown types: |
| 80 | - `false` — suppress warnings |
| 81 | - Custom function — log or report |
| 82 | |
| 83 | ### Querying PT with GROQ |
| 84 | |
| 85 | Always expand references inside custom blocks: |
| 86 | |
| 87 | ```groq |
| 88 | body[]{ |
| 89 | ..., |
| 90 | _type == "image" => { |
| 91 | ..., |
| 92 | asset-> |
| 93 | }, |
| 94 | markDefs[]{ |
| 95 | ..., |
| 96 | _type == "internalLink" => { |
| 97 | ..., |
| 98 | "slug": @.reference->slug.current |
| 99 | } |
| 100 | } |
| 101 | } |
| 102 | ``` |