- 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.
Declaring a section
sections/Faq.slurp
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 iskey: 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 namedsection into the render scope:
{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 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, andmax 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.
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
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
Ablocks { } group takes three kinds of entry, and they can be mixed:
- A bare
@themealways 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
headingkeeps it, so adding aheadingto 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.
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 afaq_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
headingwas a number, so the default was kept.alignwas not one of the options, so the default was kept.max_itemswas 999, so it was clamped tometa.max.blurbisrichtext, 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.ctais alink, andjavascript:is not an allowed scheme, so it collapsed to empty.backgroundis acolor, and that is neither a hex value nor a named colour, so the default was kept. This is what stops a colour carrying;orurl(...)into a style attribute.markeris anicon, and a space is not in[a-z0-9-], so the default was kept. It declared none, and the fallback for aniconis the empty string.ghostis not in the schema, so it was dropped.- Every setting the state did not mention is present anyway, at its default.
- The
alienblock was dropped, because its type is not one the schema declares. - The third
faq_itemwas dropped, because the schema saidmax: 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 namedHero with max_per_page: 3, the align and max_items settings from
above and one faq_item block comes out like this:
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
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.