add skills
Build and Deploy / build-and-deploy (push) Successful in 2m53s

This commit is contained in:
2026-05-18 08:39:42 +02:00
parent 92a80e8759
commit 57af0b8386
53 changed files with 10190 additions and 0 deletions
@@ -0,0 +1,111 @@
---
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
}
}
}
```
@@ -0,0 +1,124 @@
---
title: Serialize Portable Text to Astro
description: Render Portable Text in Astro using astro-portabletext
tags: [portable-text, astro, serialization, rendering]
---
# Serialize Portable Text to Astro
Use `astro-portabletext` to render PT in Astro projects. This is the officially recommended library for Sanity + Astro.
```bash
npm install astro-portabletext
```
## Basic Usage
```astro
---
import {PortableText} from 'astro-portabletext'
const {value} = Astro.props
---
<PortableText value={value} />
```
## Custom Components
Pass custom components to override default rendering:
```astro
---
import {PortableText} from 'astro-portabletext'
import ImageBlock from './ImageBlock.astro'
import CodeBlock from './CodeBlock.astro'
import Link from './Link.astro'
const {value} = Astro.props
const components = {
type: {
image: ImageBlock,
code: CodeBlock,
},
mark: {
link: Link,
},
block: {
h1: 'h1',
h2: 'h2',
blockquote: 'blockquote',
},
}
---
<PortableText {value} {components} />
```
### Custom Type Component
```astro
---
// ImageBlock.astro
const {node} = Astro.props
---
<figure>
<img src={urlFor(node).width(800).url()} alt={node.alt || ''} />
{node.caption && <figcaption>{node.caption}</figcaption>}
</figure>
```
### Custom Mark Component
```astro
---
// Link.astro
const {node} = Astro.props
const href = node?.href || ''
const rel = href.startsWith('/') ? undefined : 'noreferrer noopener'
---
<a {href} {rel}><slot /></a>
```
## Using Slots for Customization
`astro-portabletext` supports Astro's slot system for simpler customization:
```astro
---
import {PortableText} from 'astro-portabletext'
---
<PortableText value={value}>
<fragment slot="block:h1">
<h1 class="text-4xl font-bold"><slot /></h1>
</fragment>
<fragment slot="mark:strong">
<strong class="font-black"><slot /></strong>
</fragment>
</PortableText>
```
## usePortableText Helper
For more control, use the `usePortableText` render function:
```astro
---
import {usePortableText} from 'astro-portabletext'
const {value} = Astro.props
const {render} = usePortableText(value)
---
<div class="prose">
{render()}
</div>
```
## Reference
- [astro-portabletext](https://github.com/theisel/astro-portabletext)
- [Sanity + Astro guide](https://www.sanity.io/guides/sanity-astro)
@@ -0,0 +1,115 @@
---
title: Serialize Portable Text to HTML
description: Convert Portable Text to HTML strings server-side using @portabletext/to-html
tags: [portable-text, html, server-side, serialization, email]
---
# Serialize Portable Text to HTML
Use `@portabletext/to-html` for server-side HTML string generation — useful for RSS feeds, emails, static rendering, or any non-framework context.
```bash
npm install @portabletext/to-html
```
## Basic Usage
```ts
import {toHTML} from '@portabletext/to-html'
const html = toHTML(portableTextBlocks, {components})
```
## ⚠️ Security: Escape HTML
Unlike framework renderers, `toHTML` returns raw strings. **You must sanitize output.**
Use `htm` + `vhtml` for safe templating, or the built-in `escapeHTML` utility:
```ts
import {toHTML, escapeHTML, uriLooksSafe} from '@portabletext/to-html'
import htm from 'htm'
import vhtml from 'vhtml'
const h = htm.bind(vhtml)
```
## Custom Components
Components are functions returning HTML strings:
```ts
const components = {
types: {
image: ({value}) => {
return `<figure>
<img src="${escapeHTML(value.url)}" alt="${escapeHTML(value.alt || '')}" />
${value.caption ? `<figcaption>${escapeHTML(value.caption)}</figcaption>` : ''}
</figure>`
},
code: ({value}) => {
return `<pre data-language="${escapeHTML(value.language)}"><code>${escapeHTML(value.code)}</code></pre>`
},
},
marks: {
link: ({children, value}) => {
const href = value?.href || ''
if (!uriLooksSafe(href)) return children
const rel = href.startsWith('/') ? '' : ' rel="noreferrer noopener"'
return `<a href="${escapeHTML(href)}"${rel}>${children}</a>`
},
strong: ({children}) => `<strong>${children}</strong>`,
em: ({children}) => `<em>${children}</em>`,
highlight: ({children}) => `<mark>${children}</mark>`,
},
block: {
h1: ({children}) => `<h1>${children}</h1>`,
h2: ({children}) => `<h2>${children}</h2>`,
blockquote: ({children}) => `<blockquote>${children}</blockquote>`,
normal: ({children}) => `<p>${children}</p>`,
},
list: {
bullet: ({children}) => `<ul>${children}</ul>`,
number: ({children}) => `<ol>${children}</ol>`,
},
listItem: {
bullet: ({children}) => `<li>${children}</li>`,
},
}
```
## With htm/vhtml (Auto-Escaped)
Using `htm` + `vhtml` auto-escapes attribute values, preventing XSS from user content that could break out of attributes in raw template literals:
```ts
const components = {
types: {
image: ({value}) => h`<img src=${value.url} alt=${value.alt || ''} />`,
},
marks: {
link: ({children, value}) => {
if (!uriLooksSafe(value?.href || '')) return children
return h`<a href=${value.href}>${children}</a>`
},
},
}
```
## Use Cases
| Use Case | Why toHTML |
|----------|-----------|
| RSS/Atom feeds | Need raw HTML string |
| Email templates | No framework runtime |
| Static site generation | Pre-render at build time |
| API responses | Return HTML from endpoints |
| PDF generation | Feed HTML to PDF libraries |
## Reference
- [@portabletext/to-html](https://github.com/portabletext/to-html)
@@ -0,0 +1,123 @@
---
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.
```bash
npm install @portabletext/markdown
```
## Basic Usage
```ts
import {portableTextToMarkdown} from '@portabletext/markdown'
const markdown = portableTextToMarkdown(portableTextBlocks)
```
## Built-in Support
Out of the box, `portableTextToMarkdown` handles:
- Headings (h1h6)
- Paragraphs
- Bold (`**`), italic (`_`), inline code (`` ` ``), strikethrough (`~~`)
- Links (`[text](url)`)
- Blockquotes (`>`)
- Ordered and unordered lists (including nested)
- Code blocks (fenced with language)
- Horizontal rules (`---`)
- Images (`![alt](url)`)
- Tables (GFM)
## Built-in Type Renderers
The library exports default renderers for common block object types. Enable them explicitly:
```ts
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?} → ![alt](src "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:
```ts
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 `![${value.alt || ''}](${value.url})`
},
},
// 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
- [@portabletext/markdown](https://github.com/portabletext/editor/tree/main/packages/markdown)
- Part of the [portabletext/editor](https://github.com/portabletext/editor) monorepo
@@ -0,0 +1,66 @@
---
title: Extract Plain Text from Portable Text
description: Convert Portable Text to plain text strings for search, meta descriptions, and summaries
tags: [portable-text, plain-text, search, seo, extraction]
---
# Extract Plain Text from Portable Text
Every `@portabletext/*` library exports a `toPlainText()` utility. Use it for meta descriptions, search indexing, summaries, and anywhere you need raw text without markup.
## Usage
```ts
// From any framework library:
import {toPlainText} from '@portabletext/react'
// or: import {toPlainText} from '@portabletext/svelte'
// or: import {toPlainText} from '@portabletext/vue'
// or: import {toPlainText} from '@portabletext/to-html'
const plainText = toPlainText(portableTextBlocks)
```
## Common Patterns
### Meta Description
```ts
function getMetaDescription(body: PortableTextBlock[]): string {
const text = toPlainText(body)
return text.length > 160 ? text.slice(0, 157) + '...' : text
}
```
### Search Indexing
```ts
// Index document content for search
const searchableText = toPlainText(document.body)
```
### Slug Generation
```ts
import slugify from 'slugify'
const slug = slugify(toPlainText(blocks), {lower: true, strict: true})
```
### Character/Word Count
```ts
const text = toPlainText(blocks)
const wordCount = text.split(/\s+/).filter(Boolean).length
const charCount = text.length
```
## Behavior
- Extracts text from all `span` children in `block` type nodes
- Joins blocks with double newlines (`\n\n`)
- Ignores custom block types (images, code blocks, etc.)
- Strips all marks (bold, links, etc.) — returns raw text only
## Reference
- [`toPlainText` source](https://github.com/portabletext/toolkit)
@@ -0,0 +1,142 @@
---
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.
```bash
npm install @portabletext/react
```
## Basic Usage
```tsx
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
```tsx
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:
```tsx
function Body({value}) {
return <PortableText value={value} components={{
types: {image: ({value}) => <img src={value.url} />}
}} />
}
```
**Good** — defined outside or memoized:
```tsx
const components: PortableTextComponents = {
types: {image: ({value}) => <img src={value.url} />}
}
function Body({value}) {
return <PortableText value={value} components={components} />
}
```
## Plain Text Extraction
```tsx
import {toPlainText} from '@portabletext/react'
const text = toPlainText(blocks) // for meta descriptions, search indexing
```
## Tailwind Typography Shortcut
For simple blogs without custom blocks, wrap in `prose`:
```tsx
<article className="prose lg:prose-xl">
<PortableText value={value} />
</article>
```
## Reference
- [@portabletext/react](https://github.com/portabletext/react-portabletext)
- [Sanity docs: Presenting Portable Text](https://www.sanity.io/docs/presenting-block-text)
@@ -0,0 +1,134 @@
---
title: Serialize Portable Text to Svelte
description: Render Portable Text in Svelte 5 and SvelteKit using @portabletext/svelte
tags: [portable-text, svelte, sveltekit, serialization, rendering]
---
# Serialize Portable Text to Svelte
Use `@portabletext/svelte` (requires Svelte 5+) to render PT in Svelte/SvelteKit.
```bash
npm install @portabletext/svelte
```
## Basic Usage
```svelte
<script>
import {PortableText} from '@portabletext/svelte'
let {value} = $props()
</script>
<PortableText {value} components={components} />
```
## Custom Components
Svelte components receive a `portableText` prop with `value`, `global`, and `indexInParent`. Child content is passed via Svelte snippets.
### Block Styles
```svelte
<!-- Heading.svelte -->
<script>
let {portableText, children} = $props()
const {value} = portableText
</script>
{#if value.style === 'h1'}
<h1 class="text-4xl font-bold">{@render children()}</h1>
{:else if value.style === 'h2'}
<h2 class="text-3xl font-semibold">{@render children()}</h2>
{:else}
<p>{@render children()}</p>
{/if}
```
### Custom Types
```svelte
<!-- ImageBlock.svelte -->
<script>
let {portableText} = $props()
const {value} = portableText
</script>
<figure>
<img src={urlFor(value).width(800).url()} alt={value.alt || ''} />
{#if value.caption}
<figcaption>{value.caption}</figcaption>
{/if}
</figure>
```
### Mark Components (Annotations)
```svelte
<!-- Link.svelte -->
<script>
let {portableText, children} = $props()
const {value} = portableText
const href = value?.href || ''
</script>
<a {href} rel={href.startsWith('/') ? undefined : 'noreferrer noopener'}>
{@render children()}
</a>
```
## Assembling Components
```svelte
<script>
import {PortableText} from '@portabletext/svelte'
import ImageBlock from './ImageBlock.svelte'
import CodeBlock from './CodeBlock.svelte'
import Link from './Link.svelte'
let {value} = $props()
const components = {
types: {
image: ImageBlock,
code: CodeBlock,
},
marks: {
link: Link,
},
block: {
h1: ({children}) => `<h1>${children}</h1>`, // or use a component
},
}
</script>
<PortableText {value} {components} />
```
## Passing Context
Pass external data to all components via `context`:
```svelte
<PortableText
{value}
{components}
context={{dataset: 'production', footnotes}}
/>
```
Access in components via `portableText.global.context`.
## Plain Text Extraction
```js
import {toPlainText} from '@portabletext/svelte'
const text = toPlainText(blocks)
```
## Reference
- [@portabletext/svelte](https://github.com/portabletext/svelte-portabletext)
- [Sanity + SvelteKit guide](https://www.sanity.io/guides/sanity-sveltekit)
@@ -0,0 +1,125 @@
---
title: Serialize Portable Text to Vue
description: Render Portable Text in Vue 3 and Nuxt using @portabletext/vue
tags: [portable-text, vue, nuxt, serialization, rendering]
---
# Serialize Portable Text to Vue
Use `@portabletext/vue` to render PT in Vue 3 / Nuxt applications.
```bash
npm install @portabletext/vue
```
## Basic Usage
```vue
<script setup lang="ts">
import {PortableText} from '@portabletext/vue'
import type {PortableTextBlock} from '@portabletext/types'
const props = defineProps<{value: PortableTextBlock[]}>()
</script>
<template>
<PortableText :value="value" :components="components" />
</template>
```
## Custom Components
Vue components can be defined as render functions, SFCs, or JSX.
### Render Function Style (Concise)
```ts
import {h} from 'vue'
import type {PortableTextVueComponents} from '@portabletext/vue'
const components: PortableTextVueComponents = {
types: {
image: ({value}) => h('img', {src: urlFor(value).width(800).url(), alt: value.alt || ''}),
code: ({value}) => h('pre', {'data-language': value.language}, h('code', value.code)),
},
marks: {
link: ({value}, {slots}) => {
const rel = !value?.href?.startsWith('/') ? 'noreferrer noopener' : undefined
return h('a', {href: value?.href, rel}, slots.default?.())
},
highlight: (_, {slots}) => h('span', {class: 'bg-yellow-200'}, slots.default?.()),
},
block: {
h1: (_, {slots}) => h('h1', {class: 'text-4xl font-bold'}, slots.default?.()),
h2: (_, {slots}) => h('h2', {class: 'text-3xl font-semibold'}, slots.default?.()),
blockquote: (_, {slots}) => h('blockquote', {class: 'border-l-4 pl-4 italic'}, slots.default?.()),
},
list: {
bullet: (_, {slots}) => h('ul', {class: 'list-disc ml-6'}, slots.default?.()),
number: (_, {slots}) => h('ol', {class: 'list-decimal ml-6'}, slots.default?.()),
},
}
```
### SFC Style (For Complex Components)
```vue
<!-- ImageBlock.vue -->
<script setup lang="ts">
import type {PortableTextComponentProps} from '@portabletext/vue'
const props = defineProps<PortableTextComponentProps<{
asset: {_ref: string}
alt?: string
caption?: string
}>>()
</script>
<template>
<figure>
<img :src="urlFor(value).width(800).url()" :alt="value.alt || ''" />
<figcaption v-if="value.caption">{{ value.caption }}</figcaption>
</figure>
</template>
```
Then register:
```ts
import ImageBlock from './ImageBlock.vue'
const components = {
types: {
image: ImageBlock,
},
}
```
## Props Pattern
Custom components receive:
| Prop | Description |
|------|-------------|
| `value` | The block/mark data |
| `index` | Position in parent array |
| `isInline` | Whether this is an inline element |
| `renderNode` | Internal renderer (rarely needed) |
Children are passed via **slots** (`slots.default?.()`), not props.
## Plain Text Extraction
```ts
import {toPlainText} from '@portabletext/vue'
const text = toPlainText(blocks)
```
## Reference
- [@portabletext/vue](https://github.com/portabletext/vue-portabletext)
- [Sanity + Nuxt guide](https://www.sanity.io/guides/sanity-nuxt)