Files
theater-ziefen-website/.agents/skills/portable-text-serialization/rules/react.md
T
johannes.gasser 57af0b8386
Build and Deploy / build-and-deploy (push) Successful in 2m53s
add skills
2026-05-18 08:39:42 +02:00

3.5 KiB

title, description, tags
title description tags
Serialize Portable Text to React Render Portable Text in React and Next.js using @portabletext/react
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/react

Basic 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 indexing

Tailwind Typography Shortcut

For simple blogs without custom blocks, wrap in prose:

<article className="prose lg:prose-xl">
  <PortableText value={value} />
</article>

Reference