Skip to content

Section schema reference

Every built-in section is defined with defineSection({ type, schema, ... }). The type string is the discriminator stored in each section’s JSON file; the Zod schema is the source of truth for content shape.

Each block on disk has this outer envelope:

{
id: string; // unique within the site
type: string; // section type key (see below)
content: { ... }; // type-specific content
options?: { ... }; // type-specific display options
layout?: {
colSpan?: number; // only meaningful inside a container
};
}

Source: packages/primitives/src/components/sections/LinkHeading/index.tsx

A top-level section heading. Generates a navigation anchor.

FieldTypeRequiredNotes
content.headingstringYesHeading text

Source: packages/primitives/src/components/sections/SubHeading/index.tsx

A second-level heading. Excluded from the top-level nav by default.

FieldTypeRequiredNotes
content.headingstringYesHeading text
content.excludeFromNavbooleanNoWhen true, omits this heading from nav generation

Source: packages/primitives/src/components/sections/SubSubHeading/index.tsx

A third-level heading. Never appears in top-level nav.

FieldTypeRequiredNotes
content.headingstringYesHeading text
content.excludeFromNavbooleanNoWhen true, omits this heading from nav generation

Source: packages/primitives/src/components/sections/Prose/index.tsx

A rich-text body block. Content is stored as sanitized HTML.

FieldTypeRequiredNotes
content.bodystringYesHTML string; listed in richTextFields and sanitized on save

Source: packages/primitives/src/components/sections/Media/index.tsx

A single image or video block.

FieldTypeRequiredNotes
content.refSingleMediaReferenceYesImage or video reference (see below)
content.linkLinkValueNoOptional link wrapping the media
FieldTypeRequiredNotes
type"image"YesDiscriminator
imageIdstringYesMedia manifest ID; defaults to ""
captionstring | string[]NoCaption text
backgroundstringNoBackground color hint
invertFromstringNoTheme at which to invert the image
borderbooleanNoRender a border
objectFit"cover" | "contain"NoCSS object-fit

Inherits all "image" fields, plus:

FieldTypeRequiredNotes
type"video"YesDiscriminator
posterstringNoPoster image URL
autoplaybooleanNoAuto-play on load
loopbooleanNoLoop playback
mutedbooleanNoMuted by default
FieldTypeRequiredNotes
options.squarebooleanNoForce square aspect ratio
options.showCaptionbooleanNoRender the caption below the media
options.borderbooleanNoRender a border
options.objectFit"cover" | "contain"NoCSS object-fit; defaults to "contain"

Source: packages/primitives/src/components/sections/Video/index.tsx

A watchable video player — self-hosted from the media library or embedded from YouTube/Vimeo/Instagram. Instagram embeds render the post card at a kind-derived aspect ratio (reels 9:16, posts 4:5) that editors can override. Use playback "click" for content viewers press play on (controls + audio) and "ambient" for silent looping footage. For decorative loops inside grids prefer the media block; large self-hosted files require the site’s asset storage.

FieldTypeRequiredNotes
content.sourceLibrarySource | ExternalSourceYesWhere the video comes from (see below)
content.captionstringNoRendered beneath the player when options.showCaption is on
FieldTypeRequiredNotes
type"library"YesDiscriminator
ref.imageIdstringYesMedia manifest id of the video asset; defaults to ""
poster.imageIdstringNoMedia manifest id of an image used as the poster instead of the auto-generated frame. Kept as a sibling of ref (not nested inside it) so the SSR ref-baker resolves the override independently, instead of colliding with the video’s own auto-generated poster
FieldTypeRequiredNotes
type"external"YesDiscriminator
provider"youtube" | "vimeo" | "instagram"YesDerived from the pasted URL
idstringYesProvider video id parsed from the pasted URL (Instagram: the post/reel shortcode)
kind"post" | "reel" | "tv"NoInstagram only: which embed path the shortcode came from (/p/"post"). Omitted for other providers
urlstringYesThe original video page URL as pasted
widthnumberYesAspect-ratio width (from oEmbed; 16 when unknown; Instagram: 9 for reels/tv, 4 for posts — editors can override via the aspect toggle)
heightnumberYesAspect-ratio height (from oEmbed; 9 when unknown; Instagram: 16 for reels/tv, 5 for posts — editors can override via the aspect toggle)
FieldTypeRequiredNotes
options.playback"ambient" | "click"No"click" (default): poster + native controls with audio. "ambient": autoplays muted in a loop with no controls
options.showCaptionbooleanNoWhen true, render the caption beneath the player

Library source:

{
"type": "video",
"content": {
"source": { "type": "library", "ref": { "imageId": "a1b2c3d4" } },
"caption": "Behind the scenes"
},
"options": { "playback": "click", "showCaption": true }
}

Library source with a poster override (note poster is a sibling of ref, each with its own imageId):

{
"type": "video",
"content": {
"source": {
"type": "library",
"ref": { "imageId": "a1b2c3d4" },
"poster": { "imageId": "e5f6a7b8" }
}
},
"options": { "playback": "click" }
}

External source:

{
"type": "video",
"content": {
"source": {
"type": "external",
"provider": "youtube",
"id": "dQw4w9WgXcQ",
"url": "https://youtu.be/dQw4w9WgXcQ",
"width": 16,
"height": 9
}
},
"options": { "playback": "ambient" }
}

Instagram reel (aspect ratio derived from the URL kind; Instagram ignores the playback option — the embed card controls its own playback):

{
"type": "video",
"content": {
"source": {
"type": "external",
"provider": "instagram",
"id": "C1a2B3c4D5e",
"kind": "reel",
"url": "https://www.instagram.com/reel/C1a2B3c4D5e/",
"width": 9,
"height": 16
}
},
"options": {}
}

Source: packages/primitives/src/components/sections/Button/index.tsx

A call-to-action button.

FieldTypeRequiredNotes
content.textstringYesButton label
content.linkLinkValueNoDestination; see LinkValue below
content.downloadbooleanNoAdds download attribute to the rendered anchor

Source: packages/primitives/src/components/sections/Container/index.tsx

A layout wrapper that holds child blocks. Nesting is capped at depth 2 (a container may hold leaves, not other containers).

FieldTypeRequiredNotes
content.layout"grid" | "masonry"YesLayout engine: grid arranges children in fixed columns with per-child column spans; masonry flows children through a CSS multi-column gallery at their natural height (no column spans); defaults to "grid"
content.columnsnumber (int, 1–6)YesGrid: number of columns children flow across. Masonry: number of CSS multi-columns children flow through — the editor’s Columns select floors this at 2 in masonry mode (a single masonry column would just be a plain stack). Defaults to 1
content.equalizeRowHeightbooleanYesGrid only: stretch every child to the height of the tallest child in its row (best-effort — applies to children whose root element can stretch, e.g. media/cards). Ignored in masonry. Defaults to false
content.childrenSection[]YesChild blocks (any section type, each with an id and optional layout.colSpan); defaults to []
content.childDefaultsRecord<string, unknown>NoDefault options applied to children added via the editor

Masonry is columns-only (CSS columns-N multi-column flow, gap-8 + per-item mb-8 break-inside-avoid) — there is no justified-rows flow and no aspect-ratio layout engine; items flow at their natural height in real DOM order, so drag-and-drop reordering works the same as grid.

Child blocks may carry layout.colSpan (int ≥ 1). In grid layout, if colSpan exceeds columns, it is clamped to columns on the next parse. In masonry layout, layout.colSpan is stripped from every child on parse — masonry has no column spans.


Source: packages/primitives/src/components/sections/Spacer/index.tsx

An empty vertical gap. No content fields.

FieldTypeRequiredNotes
content{}Always an empty object

Source: packages/primitives/src/components/sections/Colors/index.tsx, schema.ts

A brand color palette display.

FieldTypeRequiredNotes
content.colorsColorItem[]YesOne entry per color
FieldTypeRequiredNotes
namestringNoDisplay name for the color
spacesColorSpace[] (min 1)YesOne or more color space representations

At least one field must be present.

FieldTypeRequiredNotes
hexstringNo6-digit hex (#rrggbb)
rgbstringNoFree-form RGB string
cmykstringNoFree-form CMYK string
pantonestringNoPantone name or code
FieldTypeRequiredNotes
options.labelstringNoSection label text
options.columnsnumber (int, 2–4)NoDisplay columns
options.collapsingbooleanNoEnable collapsing layout
options.showLabelbooleanNoRender the label; defaults to true in settings

Source: packages/primitives/src/components/sections/IconList/index.tsx

A list of labelled items, each with optional icon and do/don’t marker.

FieldTypeRequiredNotes
content.itemsIconListItem[]YesOne entry per list item
FieldTypeRequiredNotes
labelstringYesShort label
textstringYesBody text
iconstringNoIcon identifier
dodont"do" | "dont"NoDo/Don’t marker
FieldTypeRequiredNotes
options.iconstring | nullNoDefault icon for all items
options.showLabelbooleanNoRender item labels
options.stackTextbooleanNoStack label + text vertically
options.dodont"do" | "dont"NoWhole-list Do/Don’t. Renders every item as a green check ("do") or red x ("dont") by default; a per-item dodont or a per-item icon overrides it. Inserted ready-made by the “Do / Don’t list” Add-menu preset.

Source: packages/primitives/src/components/sections/DoDontMedia/index.tsx

A media block with an explicit Do / Don’t annotation. Designed to be used inside a container alongside other dodont_media blocks.

FieldTypeRequiredNotes
content.refSingleMediaReferenceYesImage or video reference; same shape as media
content.dodont"do" | "dont"YesAnnotation shown on the block
content.linkLinkValueNoOptional link wrapping the media
FieldTypeRequiredNotes
options.squarebooleanNoForce square aspect ratio
options.showCaptionbooleanNoRender the caption below the media
options.borderbooleanNoRender a border
options.objectFit"cover" | "contain"NoCSS object-fit; defaults to "contain"

Source: packages/primitives/src/components/sections/Document/index.tsx

One downloadable/viewable file — a PDF or a single self-contained HTML file — presented as a clickable row. Clicking opens it in a full-viewport preview overlay without leaving the page; on phones a PDF opens in the OS viewer instead. Pick the file from the media library (documents tab); leave the title blank to show the file name without its extension, or type one to override it. Leave “Show in Documents menu” on for anything a viewer should be able to reach from any page — those entries collect into a derived Documents group at the bottom of the left nav. Use one section per file; for a set of downloads, place several document sections in a container.

Bytes are auth-gated end to end: the row and the overlay both read /api/document-file/{documentId}/{filename}, which enforces the site gate plus the referencing section’s and page’s access. File metadata (name, size, kind) lives in the src/content/document-manifest.json sidecar, keyed by content.doc.documentId — never in image-manifest.json.

FieldTypeRequiredNotes
content.titlestringYesRow title. Blank renders the file name without its extension — a display fallback, never written back into content
content.doc.documentIdstringYesHash key into the document manifest. Deliberately not imageId — documents are not resolved by the media ref-baker
content.showInDocumentsNavbooleanNoDefaults to true. When on, the document joins the derived “Documents” group at the bottom of the left nav

None.


Defined in packages/primitives/src/schemas/link.ts. A discriminated union on kind.

External link (kind: "external"):

FieldTypeNotes
kind"external"
hrefstringMust be empty, relative, http(s)://, or mailto:; dangerous schemes (javascript:, data:) are rejected
target"_self" | "_blank"

Internal link (kind: "internal"):

FieldTypeNotes
kind"internal"
pageIdstringID of the target page
anchorSectionIdstring | null | undefinedOptional in-page anchor
target"_self" | "_blank"