This commit is contained in:
@@ -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 (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:
|
||||
|
||||
```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?} → 
|
||||
'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 ``
|
||||
},
|
||||
},
|
||||
|
||||
// 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)
|
||||
Reference in New Issue
Block a user