Adding a block type
The exact steps to add a new page-builder block, and the parallel pattern for form field kinds.
Every block type is looked up by a single typestring, both by the public page renderer and the dashboard builder canvas — adding a new one never requires either of those to change. The steps below are numbered in the order you’d actually do them.
Steps
- Add the config type and a union member in
src/lib/blocks/types.ts. Every other block config is defined the same way — seeCustomHtmlConfigatsrc/lib/blocks/types.ts:242— and theBlockInstancediscriminated union that every block adds one arm to starts atsrc/lib/blocks/types.ts:292. - Create
src/lib/blocks/blocks/<name>.tsxexporting a default config, anEditcomponent, and aRendercomponent. Use an existing block as the template —src/lib/blocks/blocks/custom-html.tsxexportscustomHtmlDefaultConfigatsrc/lib/blocks/blocks/custom-html.tsx:9,CustomHtmlEditatsrc/lib/blocks/blocks/custom-html.tsx:16, andCustomHtmlRenderatsrc/lib/blocks/blocks/custom-html.tsx:91. - Add one entry to
BLOCK_REGISTRYinsrc/lib/blocks/registry.tsx(the object starts atsrc/lib/blocks/registry.tsx:78; thecustom-htmlentry atsrc/lib/blocks/registry.tsx:150is the shape to copy: type label, default config, Edit, Render). - Add the type string to
BLOCK_TYPESinsrc/lib/schemas/page-schema.ts:12— this is the Zod enum that validates every stored block’stypefield; a block type missing here gets silently dropped byparsePageSchemathe moment a page with it is loaded.
That’s the whole extension point. Import it and register it once each:
export type BlockDefinition<C> = {
type: BlockType;
label: string;
defaultConfig: C;
Edit: ComponentType<{
config: C;
onChange: (next: C) => void;
childBlocks?: BlockInstance[];
renderChildList?: () => ReactNode;
event?: EventRecord;
onEventFieldsChange?: (patch: Partial<EventRecord>) => void;
availableForms?: FormRecord[];
}>;
Render: ComponentType<{
config: C;
ctx: PageRenderContext;
renderedChildren?: ReactNode[];
}>;
};childBlocks/renderChildListare only read by the container block’s Edit; event/onEventFieldsChangeonly by blocks (like hero) that edit the event’s own fields directly; availableFormsonly by the form block’s “which form?” dropdown. A new block’s Edit simply ignores whichever of these it doesn’t need.
The parallel pattern for form fields
The generic Forms system (src/lib/forms/) is explicitly modeled on the same shape, applied to form field kinds instead of page blocks: a FieldTypeDefinition type (src/lib/forms/registry.tsx:45) pairs a config type with an Edit/Input component pair and one FieldValidator subclass, all looked up from a single FIELD_TYPE_REGISTRY table (src/lib/forms/registry.tsx:68). Adding a new field kind means: a config type in src/lib/forms/types.ts, a validator extending FieldValidator (the abstract base at src/lib/forms/validators/base.ts:10 implements the shared required/empty short-circuit once — a subclass only implements its own isEmpty and validateValue), an Edit+Input pair under src/lib/forms/fields/*.tsx, and one entry in FIELD_TYPE_REGISTRY plus the kind string in FIELD_KINDS (src/lib/forms/registry.tsx:173).
Good to know