Skip to content

{% schema %}, node schemas

The schema declaration inside every node file, and the 36 setting types.

Updated View as Markdown

Every editor-manageable node ends with a {% schema %} block containing a JSON document. The engine parses it into a NodeSchema (crates/ringroad-renderer/src/section/mod.rs) and registers it in the theme manifest under the node’s type name. The editor reads these schemas to build the “Add section” panel, the settings forms, and the child- placement rules.

{% schema %}
{
  "name": "Hero",
  "tag": "section",
  "class": "rr-hero-wrapper",
  "limit": 1,
  "settings": [ … ],
  "slots": { … },
  "presets": [ … ]
}
{% endschema %}

Node schema fields

Field Type Default Meaning
name string , Human-readable node name (required).
role "section" | "simple" "section" Editor placement. Prefer the @editor: marker over this field; both work.
tag string "div" Wrapper element: section, div, header, footer, article, aside.
class string , Extra class appended to the wrapper (rr-node rr-node--{type} {class}).
limit integer , Max instances per page/group (e.g. 1 for hero).
settings array [] The node’s settings definitions (see below).
accepts array [] Allowed child types. Entries are type names, "@theme" (any theme node), or "@app" (app-injected nodes, stub today).
max_children integer 50 Max children in the implicit default slot.
slots object , Named slots instead of the implicit default slot; each maps to { "accepts": [...], "max": n }.
presets array [] Pre-built configurations shown in the “Add section” panel.
default object , The preset used when the node is instantiated (optional).
locales object {} Per-locale label overrides.
enabled_on / disabled_on array , Page-type allow/deny lists for the editor.

accepts, child type specs

"accepts": ["slide", "@theme", "@app"]
  • "slide", exactly the node type slide.
  • "@theme", any node registered in the theme (expanded at load time to every type not already listed).
  • "@app", app-injected nodes. Stub today: expand_app_in_accepts removes the marker without injecting anything (section/registry.rs).

slots, named child buckets

When a node declares slots, children are placed into named buckets instead of the single default slot. Each child instance carries a slot field; the template reads pre-rendered slot HTML from node.slots.{name}.

The default theme’s hero node uses two slots:

"slots": {
  "col": { "accepts": ["@theme"] },
  "cta": { "accepts": ["button"], "max": 3 }
}
{{ node.slots.col }}
{% if node.slots.cta %}<div class="rr-hero__actions">{{ node.slots.cta }}</div>{% endif %}

presets, “Add section” starting points

Each preset pre-fills the node (and its children) when a merchant adds it:

"presets": [
  {
    "name": "Default",
    "type": "hero",
    "settings": {},
    "children": [
      {
        "type": "button",
        "slot": "cta",
        "settings": { "label": "Shop now", "url": "/products", "style": "primary" }
      }
    ]
  }
]

children accepts the old "blocks" key as an alias.

Setting types

Settings are deserialized from the type field via #[serde(tag = "type", rename_all = "snake_case")] into one of 36 SettingDefinition variants (src/schema/types.rs). Every definition has id (the machine key, used in node.settings.{id}) and label; most have info (help text) and type-specific default.

Basic inputs

type Config fields Default rule
checkbox default: bool? false
number default: f64?, placeholder null
radio default: string?, options: [{value,label}] first option
range default (required), min, max, step?, unit? the default
select default: string?, options, group? first option
size default: string?, unit?, placeholder null
text default: string?, placeholder null
textarea default: string?, placeholder null
{ "type": "select", "id": "height", "label": "Height",
  "options": [ { "value": "auto", "label": "Auto" }, { "value": "large", "label": "Large" } ],
  "default": "large" }

size is a free-form CSS length — any unit (200px, 100%, 10rem, 60vw), the keywords auto / none / initial / inherit / unset, unitless 0, or a CSS function with balanced parens (calc(100% - 2rem), clamp(1rem, 5vw, 3rem), var(--gap)). Values are validated at schema load, save, and render time, so templates can emit node.settings.{id} verbatim into style="..." without appending a unit:

{% if node.settings.width %}width: {{ node.settings.width }};{% endif %}

The optional unit (default "px") is what the editor preselects, and what legacy bare numbers (stored by the old px range settings, e.g. 500) are re-emitted as during render-time merging. An empty default means “unset” and emits nothing.

Resource pickers

type Config fields
product, collection, page, article, blog id, label, info?
product_list, collection_list, article_list id, label, limit?, info?

Resource pickers have no default, the merchant must pick.

Color

type Config fields Where
color default: string?, placeholder node settings
color_background default: string? node settings (solid or gradient)
color_palette default: { key: hex }, 2–20 colors theme-level only
color_scheme default: string? section-level scheme picker
color_scheme_group definition: ColorSchemeField[], default_scheme? theme-level only

color_palette + color_scheme_group together drive the color cascade, see The color cascade.

Text & media

type Config fields Notes
font_picker default (required, e.g. "inter_n4")
html default?, placeholder Raw HTML
image_picker id, label Returns a URL
inline_richtext / richtext default? Rich text
link_list default? Menu handle picker
code language (required), default? Max 50 KB
metaobject / metaobject_list metaobject_type (required), limit?
text_alignment default? left / center / right / justify
url id, label
video id, label Media-library picker
video_url accept?: ["youtube","vimeo",…] URL input

Layout / sidebar (no value output)

type Config fields
header content (heading text)
paragraph content (help text)
group label, info?, settings: SettingDefinition[]

These render as structural UI in the settings panel and produce no setting value.

Structural groups

{
  "type": "group",
  "label": "Content",
  "settings": [
    { "type": "text", "id": "heading", "label": "Heading" },
    { "type": "textarea", "id": "subtext", "label": "Subtext" }
  ]
}

group is a collapsible titled section that organizes other settings in the editor’s settings panel. It is structural only: groups contribute no key to the settings map, members are flattened depth-first (document order) before defaults are applied or values validated, and groups nest recursively (a group’s settings may contain further groups). The editor renders each group as a titled row with a collapse chevron; members appear beneath it. Usable in both theme.json global settings and node {% schema %} blocks (see themes/default/nodes/hero.jinja for a real example).

Validation and defaults

At theme load, every setting passes SettingDefinition::validate():

  • range: min < max; default within [min, max].
  • select / radio: at least one option; default, if set, must be an option value.
  • color_palette: 2–20 colors; keys must start with a letter.
  • font_picker: default non-empty.
  • metaobject / metaobject_list: metaobject_type non-empty.

At render time, apply_setting_defaults merges the merchant’s stored values with schema defaults: missing keys are inserted, and stored values that fail type validation are replaced by the default (invalid checkbox values fall back to false, invalid numbers to the default, lists capped at 50 entries, code capped at 50 KB). Because of this, node templates can reference node.settings.{id} without | default() guards, the default is always present.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close