Schema
The <schema> block is JSON. It says what the section is, what a creator can change about it, which blocks it may hold, and how Add Section offers it. The builder's settings panel for a section is generated from its schema and nothing else. There is no per-section panel code to write: declare a setting and its control appears.
{
"schema_version": 1,
"type": "features",
"name": "Features",
"category": "Content",
"version": "1.0.0",
"settings": [
{ "type": "header", "id": "hdr_content", "label": "Content" },
{ "type": "text", "id": "heading", "label": "Heading", "default": "Why us" },
{ "type": "range", "id": "columns", "label": "Columns", "default": 3, "min": 1, "max": 4, "step": 1, "responsive": true },
{ "type": "range", "id": "padding_top", "label": "Top padding", "default": 64, "min": 0, "max": 200, "step": 4, "unit": "px" },
{ "type": "color_scheme", "id": "color_scheme", "label": "Colour scheme", "default": "white" }
],
"blocks": [
{
"type": "feature",
"name": "Feature",
"limit": 12,
"settings": [
{ "type": "icon_picker", "id": "icon", "label": "Icon", "default": "star" },
{ "type": "text", "id": "title", "label": "Title", "default": "Fast shipping" },
{ "type": "textarea", "id": "text", "label": "Text", "default": "" }
]
}
],
"max_blocks": 12,
"presets": [
{ "name": "Three features", "blocks": [{ "type": "feature" }, { "type": "feature" }, { "type": "feature" }] }
],
"capabilities": []
}Top level#
A key the engine does not recognise is kept and written back untouched. Nothing you add is stripped.
A setting#
header, paragraph and divider draw in the panel and hold no value. They take no default.
Every control type#
This list is generated from the builder's control registry, the same one the compiler validates against and the panel draws from. If a type is here, it exists; if it is not, it does not.
A working declaration of each of the 25 types
[
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "Welcome"
},
{
"type": "textarea",
"id": "body",
"label": "Body"
},
{
"type": "richtext",
"id": "content",
"label": "Content"
},
{
"type": "number",
"id": "columns",
"label": "Columns",
"min": 1,
"max": 6,
"default": 3
},
{
"type": "toggle",
"id": "show_badge",
"label": "Show badge",
"default": true
},
{
"type": "checkbox",
"id": "agree",
"label": "Require agreement"
},
{
"type": "select",
"id": "layout",
"label": "Layout",
"options": [
{
"label": "Centre",
"value": "center"
},
{
"label": "Split",
"value": "split"
}
],
"default": "center"
},
{
"type": "radio",
"id": "style",
"label": "Button style",
"options": [
{
"label": "Filled",
"value": "filled"
},
{
"label": "Outline",
"value": "outline"
}
],
"default": "filled"
},
{
"type": "range",
"id": "padding",
"label": "Padding",
"min": 0,
"max": 160,
"step": 4,
"unit": "px",
"default": 64,
"responsive": true
},
{
"type": "color",
"id": "background",
"label": "Background",
"default": "#ffffff"
},
{
"type": "color_scheme",
"id": "scheme",
"label": "Colour scheme",
"default": "white"
},
{
"type": "font_picker",
"id": "font",
"label": "Heading font",
"default": "Inter"
},
{
"type": "alignment",
"id": "align",
"label": "Align",
"default": "center"
},
{
"type": "image_picker",
"id": "image",
"label": "Image"
},
{
"type": "image_gallery",
"id": "images",
"label": "Images",
"max": 12
},
{
"type": "video_picker",
"id": "video",
"label": "Background video"
},
{
"type": "file_picker",
"id": "download",
"label": "Downloadable file"
},
{
"type": "icon_picker",
"id": "icon",
"label": "Icon",
"default": "zap"
},
{
"type": "url",
"id": "button_url",
"label": "Button link",
"default": "/shop"
},
{
"type": "page_picker",
"id": "page",
"label": "Page"
},
{
"type": "product_picker",
"id": "product",
"label": "Product"
},
{
"type": "community_picker",
"id": "community",
"label": "Community"
},
{
"type": "header",
"id": "hdr_content",
"label": "Content"
},
{
"type": "paragraph",
"id": "note",
"content": "Shown above the fold on every page."
},
{
"type": "divider",
"id": "div_1"
}
]Older type names#
Early sections used different names for some types. They still validate and mean the same thing, but new sections should use the names above.
How a value is resolved#
A page stores only what the creator changed. Everything else is decided in one order, every time a section renders:
resolved = saved value ?? schema default ?? control fallbackFor a block setting, the block's saved value comes first, then the block definition's default, then the fallback. The theme is not a step in this chain: it provides tokens that a template reads directly, and never overrides a setting by name.
Responsive values arrive as { desktop, tablet, mobile }, filled downwards. Set desktop only and tablet and mobile inherit it.
saved: { "columns": { "desktop": 4, "mobile": 1 } }
resolved: { "columns": { "desktop": 4, "tablet": 4, "mobile": 1 } }Read one breakpoint in the template as {{ settings.columns.desktop }}, or pass all three to CSS as custom properties and pick with container queries.
Saving strips defaults. A value equal to the default is not stored, and a responsive value equal at every breakpoint collapses to a plain one. That keeps pages small, and it is why changing a default reaches every instance that never overrode it.
Assets#
An image, video, file or gallery setting holds an asset reference, asset://<sha256>, never a URL. By the time the template runs, the reference has been turned into the URL this account serves the file from, so you print it directly:
{% if settings.image != "" %}
<img src="{{ settings.image }}" alt="{{ settings.image_alt }}" width="1200" height="800">
{% endif %}A reference held anywhere else goes through the asset filter: {{ block.settings.clip | asset }}. A reference the account does not have resolves to an empty string, so an empty image means "no image chosen", never an error. Because no stored page knows where bytes live, a theme moves between accounts and storage can change without a page being touched.
Changing a section safely#
Setting ids and block types are the contract between a section file and every page that uses it. An update may freely:
- add settings and block types; existing instances get the defaults,
- change defaults; instances that never overrode them follow,
- change markup and CSS.
It must not rename or remove an id without saying so. Declare the rename and the old value is read under the new name:
"version": "1.1.0",
"migrations": [
{ "from": "title", "to": "heading", "since": "1.1.0" }
]A removed setting's saved value is kept as an unknown key, so putting the setting back restores it.
What the validator refuses#
Each refusal names the path, such as settings[3].default, and the line in the file:
- a
typethat is not a control type, listing the ones that are, - a bad or duplicate
id, optionsmissing on aselectorradio, or a default that is not among them,- a default of the wrong type, such as
default "big" is not a range, responsiveon a type that cannot be,- a
rangewithoutminandmax, or withminnot belowmax, - a preset that sets a setting or adds a block the section does not declare,
- a block type declared twice,
- both
allowed_templatesanddisabled_templates, - a
schema_versionnewer than the engine, - more settings than the limit.