Files
theater-ziefen-website/.agents/skills/portable-text-conversion/rules/manual-construction.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

4.9 KiB

title, description, tags
title description tags
Manually Construct Portable Text Blocks Build Portable Text blocks programmatically from any data source
portable-text
construction
api
programmatic
migration

Manually Construct Portable Text Blocks

Build PT blocks directly when converting from non-HTML sources (APIs, databases, custom formats) or when you need precise control over the output.

Key Generation

Every block, span, and markDef needs a unique _key:

import {randomKey} from '@sanity/util/content'

const key = randomKey(12) // e.g., "a1b2c3d4e5f6"

Or use a simple helper:

const randomKey = () => Math.random().toString(36).slice(2, 14)

Building Blocks

Simple Paragraph

{
  _type: 'block',
  _key: randomKey(),
  style: 'normal',
  children: [
    {_type: 'span', _key: randomKey(), text: 'Hello world', marks: []}
  ],
  markDefs: []
}

Heading

{
  _type: 'block',
  _key: randomKey(),
  style: 'h2', // h1, h2, h3, h4, h5, h6
  children: [
    {_type: 'span', _key: randomKey(), text: 'Section Title', marks: []}
  ],
  markDefs: []
}

Text with Decorators (Bold, Italic, Code)

{
  _type: 'block',
  _key: randomKey(),
  style: 'normal',
  children: [
    {_type: 'span', _key: randomKey(), text: 'This is ', marks: []},
    {_type: 'span', _key: randomKey(), text: 'bold', marks: ['strong']},
    {_type: 'span', _key: randomKey(), text: ' and ', marks: []},
    {_type: 'span', _key: randomKey(), text: 'italic', marks: ['em']},
    {_type: 'span', _key: randomKey(), text: ' text.', marks: []},
  ],
  markDefs: []
}

Annotations require a markDef entry and a matching key in marks:

const linkKey = randomKey()

{
  _type: 'block',
  _key: randomKey(),
  style: 'normal',
  children: [
    {_type: 'span', _key: randomKey(), text: 'Visit ', marks: []},
    {_type: 'span', _key: randomKey(), text: 'Sanity', marks: [linkKey]},
    {_type: 'span', _key: randomKey(), text: ' for more.', marks: []},
  ],
  markDefs: [
    {_type: 'link', _key: linkKey, href: 'https://www.sanity.io'}
  ]
}

Overlapping Marks

A span can have multiple marks (both decorators and annotations):

const linkKey = randomKey()

// "bold link" — both strong and linked
{_type: 'span', _key: randomKey(), text: 'bold link', marks: ['strong', linkKey]}

Lists

Lists are regular blocks with listItem and level:

// Bullet list
[
  {
    _type: 'block', _key: randomKey(), style: 'normal',
    listItem: 'bullet', level: 1,
    children: [{_type: 'span', _key: randomKey(), text: 'First item', marks: []}],
    markDefs: []
  },
  {
    _type: 'block', _key: randomKey(), style: 'normal',
    listItem: 'bullet', level: 1,
    children: [{_type: 'span', _key: randomKey(), text: 'Second item', marks: []}],
    markDefs: []
  },
  {
    _type: 'block', _key: randomKey(), style: 'normal',
    listItem: 'bullet', level: 2, // nested
    children: [{_type: 'span', _key: randomKey(), text: 'Nested item', marks: []}],
    markDefs: []
  },
]

Custom Block Types

Any object with _type and _key can be a block:

// Image block
{
  _type: 'image',
  _key: randomKey(),
  asset: {_type: 'reference', _ref: 'image-abc123-800x600-png'},
  alt: 'A description',
}

// Code block
{
  _type: 'code',
  _key: randomKey(),
  language: 'typescript',
  code: 'const x = 42',
}

// YouTube embed
{
  _type: 'youtube',
  _key: randomKey(),
  url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
}

Helper Function

A utility for building common blocks:

function createBlock(
  text: string,
  style: string = 'normal',
  options?: {listItem?: string; level?: number}
) {
  return {
    _type: 'block',
    _key: randomKey(),
    style,
    ...(options?.listItem ? {listItem: options.listItem, level: options.level || 1} : {}),
    children: [{_type: 'span', _key: randomKey(), text, marks: []}],
    markDefs: [],
  }
}

// Usage
const blocks = [
  createBlock('Introduction', 'h2'),
  createBlock('This is a paragraph.'),
  createBlock('First point', 'normal', {listItem: 'bullet', level: 1}),
  createBlock('Second point', 'normal', {listItem: 'bullet', level: 1}),
]

Validation Checklist

Before writing PT blocks to Sanity, verify:

  • Every block has _type and _key
  • Every span has _type: "span", _key, text, and marks
  • Every annotation key in marks[] has a matching entry in markDefs[]
  • markDefs entries have _type and _key
  • _key values are unique within the array
  • style values match your schema's allowed styles
  • listItem values match your schema's allowed list types
  • Custom block _type values match registered schema types

Reference