Skip to main content
A section is a template that declares its own editable surface. The declaration sits in frontmatter, and a host application turns it into a settings form. An editor user can change a heading, pick an alignment, swap an image or reorder a list of questions without the template being touched. The division of responsibility:
  • The template owns the markup and says which values are editable.
  • The host stores what the editor saved and merges it over the schema.
  • The editor user only ever sees a form.
With the schema in the template, the form and the markup cannot drift apart.

Declaring a section

sections/Faq.slurp
Three things are declared. name is the label an editor shows in its “add a section” list. settings are the fields on the form. blocks are the repeatable child items, each with its own settings, which is how a list a merchant can add to and reorder is expressed. A section must have a name. Leaving it out is error[InvalidFrontmatter]: section block requires a name.

The nine setting kinds

Each entry is key: kind, optionally = default, optionally followed by a { meta } object. Only label, min and max have meaning in meta; anything else is carried through for the editor to use.
A value that fails one of those checks never raises. Depending on the kind it either falls back to the declared default (text, color, select, toggle, and a number that is not a number) or is neutralised in place (link and image collapse to empty, richtext is sanitized, number is clamped). See merging below.
select options may be bare identifiers or quoted strings, and the two are interchangeable: select(left, center) and select("left", "center") both work. A default must be one of the options, or the build fails with Default 'middle' for setting 'align' is not one of the select options.

How the values reach the template

The host injects one object named section into the render scope:
Blocks are an array, so {each} walks them and every one carries item.settings, its own id and type, and its own blocks array. Settings are a plain object, so a missing key is null and renders as the empty string, like any other path. Rendering the section above with a saved heading, one filled block and one empty one produces:
The second block shows ?, the schema default for question, because the editor never filled it in. That is the merge substituting the default.

Blocks

A block is a repeatable child. The simplest form declares it inline, as in the FAQ above, and max caps how many of that type one section may hold. Once more than one section wants the same block, move it into its own file under blocks/:
blocks/heading.slurp
block { } is the same grammar as section { } minus max_per_page, and it does not require a name. Inside a block file the root object is called block, not section.
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 label for the editor. A file name outside [A-Za-z0-9_-], including anything nested a directory deeper, is not a block at all and is ignored.

Rendering block files with {blocks}

{each} over section.blocks works, but it puts every block’s markup inside the section that hosts it. {blocks} renders each child from its own file instead:
sections/Blank.slurp
Each child is wrapped in a display: contents element carrying its id, so an editor’s click-to-select can resolve it without the wrapper affecting layout. {blocks} renders the children of the nearest enclosing section or block, preferring the block. That is how nesting works: a group block whose file contains {blocks} renders its own children. A block whose file hosts its own type is skipped by a cycle guard rather than recursing.

Which blocks a section accepts

A blocks { } group takes three kinds of entry, and they can be mixed:
Two rules:
  • A bare @theme always wins over a whitelist, whatever order they appear in. Widening is never silently narrowed.
  • An inline definition always beats the shared catalog. A section that declares its own heading keeps it, so adding a heading to the theme palette can never change how an existing section renders.
@theme() with no names is an error, and the message names the bare @theme form.
A @theme block with no catalog is dropped from the render. The catalog is built by the host from the theme’s blocks/ files and passed into the merge. Without it, a targeted block is an unknown type, so the merge drops it while the saved state survives untouched in storage. The page renders as though nothing had been added, and nothing is reported. This is a host integration bug rather than a template one.

The merge is tolerant

Saved editor state is merged over the schema, never trusted as-is. The rule is that a theme upgrade must not be able to break a stored page, and a stored page must not be able to break a render. So every mismatch resolves to something valid rather than to an error. A worked merge. The schema is the nine-kind example above, plus a faq_item { max: 2 settings { question: text answer: richtext } } block. The saved state is wrong in every way at once:
saved state
what the template sees
The rules, line by line:
  • heading was a number, so the default was kept.
  • align was not one of the options, so the default was kept.
  • max_items was 999, so it was clamped to meta.max.
  • blurb is richtext, so it was sanitized and the script is gone. That happens at the merge, which is why even a bare {html} of a richtext setting is safe.
  • cta is a link, and javascript: is not an allowed scheme, so it collapsed to empty.
  • background is a color, and that is neither a hex value nor a named colour, so the default was kept. This is what stops a colour carrying ; or url(...) into a style attribute.
  • marker is an icon, and a space is not in [a-z0-9-], so the default was kept. It declared none, and the fallback for an icon is the empty string.
  • ghost is not in the schema, so it was dropped.
  • Every setting the state did not mention is present anyway, at its default.
  • The alien block was dropped, because its type is not one the schema declares.
  • The third faq_item was dropped, because the schema said max: 2.
id and type are validated too. They come from the editor and get rendered into attributes, CSS selectors and JavaScript strings by templates the engine does not control, so a value carrying a quote, an angle bracket, a backslash, a backtick or whitespace is dropped rather than rewritten.

Limits

max_per_page on a section is editor metadata: it is parsed and published in the schema, and nothing in the compiler enforces it. It is legal on section only, not on block.

The schema a host reads

A host extracts the declaration as JSON and builds its form from it. A section named Hero with max_per_page: 3, the align and max_items settings from above and one faq_item block comes out like this:
The control to render comes from type, the initial value from default, the label from meta.label, and the constraints from options or from meta.min and meta.max. Absent keys are omitted rather than emitted as null, so a setting with no default has no default key. In Rust that is slurp_compiler::extract_schema for a section and extract_block_schema for a block file. The matching render entry point is render_section, which performs the merge and injects the result before rendering. Use render_section_with_registry_and_blocks when the theme has blocks/ files: that is the variant that takes the block catalog, without which @theme blocks are dropped. See Embedding with Rust.

Rendering without a host

slurp build has no concept of a section, but section is an ordinary context key, so --globals can supply one and render a section template:
data.json
This path does not merge. What you write in --globals is exactly what the template sees, so defaults are not filled in, numbers are not clamped, unknown block types are not dropped and richtext is not sanitized. It is a good way to see markup, and a bad way to reason about the schema. The merge only runs when a host calls render_section.
slurp build also writes an HTML file for every .slurp file outside components/ and layouts/, so blocks/heading.slurp produces a dist/blocks/heading.html that can be ignored.

Next

Schema reference

The grammar, every key, and the serialized shape.

Embedding with Rust

extract_schema, render_section, and building the block catalog.