Skip to main content

Overview

The block system is split across two packages: The split exists because the orchestrator (Node/Fastify) needs block schemas for validation and AI planning but has no React dependency. Renderers live in a separate package so React is only pulled in where it’s needed.

How a Block Is Defined

Every block has three parts, co-located in a single file in packages/shared/src/blocks/:

Schema

The Zod object defines the data shape — field types, enums with defaults, required vs optional, array constraints. This is the source of truth for validation. The orchestrator runs validateBlockProps() against this schema before applying any operation.

Metadata (meta)

Rich metadata layered on top of the schema:
  • fields — per-field FieldMeta with kind, label, imageSpec, inlineEditable
  • listFields — describes array fields (like cards in CardGrid) with item-level field metadata
  • displayName, description, category — used by the editor’s block picker and property panel
  • chrome — if true, the block is structurally pinned (e.g. SiteHeader, Footer) and cannot be added, moved, or removed

Default Props

An exported function (e.g. bannerDefaultProps()) that returns sensible starter content. Used when the AI or user adds a new block — the defaults are the starting point that the AI then modifies.

Field Metadata Vocabulary

The _helpers.ts file provides factory functions for declaring field metadata:
The kind field drives behavior across the stack:

How a Block Is Rendered

Renderers live in packages/blocks/src/blocks/{type}/renderer.tsx:
Key patterns:
  • Untyped props — renderers accept Record<string, unknown> and coerce to safe types. Validation happens upstream in the orchestrator.
  • data-editable-target — marks DOM elements for inline editing in the preview overlay. The value matches a prop key.
  • No imports from shared (usually) — renderers are stateless view functions. They don’t validate or re-fetch metadata.

How They Connect

Schemas and renderers are joined by type name convention — the string "Banner" passed to registerBlock() must match the key in the renderers map: SharedBlockRenderer is the glue:
It checks the built-in renderer map first, then falls back to custom renderers registered at runtime (for site-specific blocks from migrations or CMS integrations).
There is no compile-time check that every schema has a matching renderer. If you add a schema in shared but forget the renderer in blocks, the block will validate but render as empty. The block catalogue page (/catalogue) is the easiest way to verify all blocks render correctly.

The Registry Singleton

The registry uses globalThis to ensure a single instance survives Next.js webpack module duplication across RSC, SSR, and API route layers:
Without this, registerBlock() in a custom block file would populate a different registry copy than getBlockMeta() reads — blocks would appear registered but metadata would be missing.

Runtime Queries

The registry exposes query functions used across the stack:

Block Manifest API

When the editor connects to a site, it fetches the block manifest from GET /api/editor/blocks. This endpoint serializes registered blocks into a JSON payload the editor and AI planner can consume:
For built-in blocks, getBlockJsonSchema() converts the Zod schema to JSON Schema and strips validation-only constraints (minLength, required, $schema, additionalProperties) — the editor only needs the structural shape. For custom blocks (external sites), the manifest is authored directly as JSON Schema in propsSchema.

Full Lifecycle


How-To Guides

Add a new block type

1

Define the schema

Create packages/shared/src/blocks/my-block.ts:
2

Register the import

Add import "./my-block.ts" to packages/shared/src/blocks/index.ts so the schema is loaded at startup.
3

Create the renderer

Create packages/blocks/src/blocks/my-block/renderer.tsx:
4

Register the renderer

Add MyBlock to the renderers map in packages/blocks/src/blocks/index.ts and to the RendererBlockType union in block-types.ts.
5

Add styles

Create packages/blocks/src/blocks/my-block/styles.css and import it from packages/blocks/src/blocks/styles.css.
6

Verify

  • pnpm typecheck — catches missing fields or type mismatches
  • Visit /catalogue on the site to see the block render with default props
  • Open the editor and ask the AI to “add a MyBlock” — the planner should pick it up from the manifest

Add a field to an existing block

1

Update the Zod schema

Add the field to the block’s z.object() in packages/shared/src/blocks/{type}.ts. Use .optional() if it’s not required.
2

Add field metadata

Add an entry to meta.fields using the f.* helpers. Choose the right kind — it determines editor UI, AI behavior, and inline editability.
3

Update default props

If the field should have a starter value, add it to the *DefaultProps() function.
4

Update the renderer

Read the new prop in the renderer component. Add data-editable-target="fieldName" if it should be inline-editable in the preview.
5

Verify

pnpm typecheck then check the block catalogue and editor.

Add a list field (repeatable items)

1

Define the array in the schema

2

Add listFields metadata

3

Update the renderer

Cast and iterate:
Note the features[0].title path format in data-editable-target — this enables inline editing of list items.

Add AI guidance for a block

If the AI makes mistakes with your block’s props (wrong enum values, missing cross-field dependencies), add a note in apps/orchestrator/src/nlp/deterministic-planner-suggestions.ts:
Notes are injected into the AI contract as natural language guidance. They’re most valuable for:
  • Enum semantics (“use center textAlign with full imagePosition”)
  • Cross-field dependencies (“full-bleed variant REQUIRES imageUrl”)
  • Richtext conventions (which markdown subset is supported)
  • Optional field toggle behavior (“omit or set empty to hide”)
See Block Schema Contracts for details on how contracts are assembled and sent to the LLM.

Add image fields with AI resolution

Mark image fields with f.image() and include an imageSpec:
The imageSpec tells the AI planner what dimensions to request from DALL-E or Unsplash. The imageAlt kind pairs with the image field for accessibility. Both are auto-detected by getImageFields() and getImageSpec() — no additional wiring needed. For list items with images, declare them in listFields.itemFields: