Skip to main content
A section or block declares its own editor surface in frontmatter. That declaration is the contract between the template and whatever renders a settings form over it.

Grammar

Commas between entries are optional and are skipped wherever they appear. section { } requires a name; omitting it is section block requires a name. block { } does not require one.
A block’s TYPE is its file name, not the name it declares. blocks/social_row.slurp is the type social_row whatever its name: says. The name is only a human label for the editor. A block file in a subdirectory (blocks/a/b.slurp) is ignored at catalog-build time, because the renderer refuses a / in a block type.

The nine setting kinds

A value that fails its kind’s check is DROPPED and the default is kept. Nothing is reported: validation happens at the merge boundary, in the host, not at compile time.

Defaults

= default is optional. Without one, the kind’s default from the table above is used. A default is written as a string, a bare identifier, a number, a negative number, a boolean or null:
A select default that is not one of its own options is an error:
select(...) options may be identifiers OR string literals, and so may the default. select(left, center) and select("left", "center") parse identically. Bare identifiers are not required.select() with no options is select needs at least one option.

Meta

A trailing { ... } object carries arbitrary keys. Three have meaning:
Any other key is stored and passed through untouched.

Colour values

color accepts a #hex of exactly 3, 4, 6 or 8 hex digits, or a CSS colour keyword from a real allowlist (the named colours plus transparent, currentcolor, inherit, initial). Anything else keeps the default, so a colour setting can never carry ;, ( or url(...) into an attribute or a style context. Both run through the same URL gate. A value with no scheme (a relative path, an anchor, a query string) is kept as written. A value WITH a scheme must be http, https, mailto or tel; anything else collapses to "". image gets this gate too, validated where the value ENTERS rather than where a theme uses it, because a theme is free to put it in background:url(...) where no render-site check would fire.

Uniqueness and bounds at parse time

A file may carry both a section { } and a block { }.

Nested blocks

A block definition may declare its own blocks { } group, recursively. The nesting is bounded by the parser’s depth limit of 64.

@theme targeting

Inside a blocks { } group, @theme says which THEME-LEVEL blocks (the ones in blocks/*.slurp) may be inserted here. Two resolution rules:
  • A bare @theme always wins over a whitelist, regardless of the order the two appear in. Widening is never silently narrowed.
  • An inline block definition always beats the shared theme catalog, so adding a block to the palette can never change an existing section’s behaviour.

Merge semantics

Saved editor state is merged over the schema at render time, TOLERANTLY, so a theme upgrade can never break stored state: None of this is reported. The saved state itself is never modified; the merge only decides what the render sees. id and type on a section, and id on a block, pass a separate character gate because they are editor-minted strings that templates put into attributes, CSS selectors and JS strings. A value carrying a quote, an angle bracket, a backslash, a backtick or whitespace is DROPPED rather than rewritten.
Without a block catalog, a @theme-targeted block is an unknown type and is DROPPED FROM THE RENDER while surviving intact in storage. The page renders as though nothing was added, and nothing is reported.The catalog is threaded through the merge rather than defaulted. It is a host integration concern, not something a template author can guard against.

Hard ceilings

The 200 is a defensive ceiling, not an editor rule: a schema max is the real per-type cap, and its absence means “as many as the editor allows” up to this bound.

Reading a schema from a template

section is the merged object { id, type, settings, blocks }. Inside a block file the same shape is bound to block instead, which is what lets {blocks} nest.
slurp build supplies no section state, so it applies no schema defaults. A page whose section { } declares heading: text = "Welcome" renders ${ section.settings.heading } as the empty string under the CLI, not as Welcome, and {blocks} renders nothing.Defaults are applied by the host’s render_section call, which is what merges saved state over the schema. Do not read a blank CLI render as a broken schema.