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 inpackages/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 runsvalidateBlockProps() against this schema before applying any operation.
Metadata (meta)
Rich metadata layered on top of the schema:
fields— per-fieldFieldMetawithkind,label,imageSpec,inlineEditablelistFields— describes array fields (likecardsin CardGrid) with item-level field metadatadisplayName,description,category— used by the editor’s block picker and property panelchrome— iftrue, 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:
kind field drives behavior across the stack:
How a Block Is Rendered
Renderers live inpackages/blocks/src/blocks/{type}/renderer.tsx:
- 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:
The Registry Singleton
The registry usesglobalThis to ensure a single instance survives Next.js webpack module duplication across RSC, SSR, and API route layers:
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 fromGET /api/editor/blocks. This endpoint serializes registered blocks into a JSON payload the editor and AI planner can consume:
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
/catalogueon 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 inapps/orchestrator/src/nlp/deterministic-planner-suggestions.ts:
- Enum semantics (“use
centertextAlign withfullimagePosition”) - 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”)
Add image fields with AI resolution
Mark image fields withf.image() and include an imageSpec:
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: