Skip to content

Nodes, schemas, page configs, rendering

The section module, node schemas, the map+order page config, resolution, and the renderer.

Updated View as Markdown

The section/ module models everything the editor and renderer agree on: node schemas (from {% schema %}), page configurations (the persisted JSON), and the recursive node renderer. Sections and blocks are the same thing now, a node, with a role hint for the editor.

NodeSchema

Parsed from each node file’s {% schema %} (fields documented in Schema blocks):

pub struct NodeSchema {
    pub role: NodeRole,            // Section | Simple (presentation hint only)
    pub name: String,
    pub tag: HtmlTag,              // section | div | header | footer | article | aside
    pub class: Option<String>,
    pub limit: Option<u32>,
    pub settings: Vec<SettingDefinition>,
    pub accepts: Vec<ChildSpec>,   // Named(type) | AnyTheme("@theme") | AnyApp("@app")
    pub max_children: u32,         // default 50
    pub slots: Option<HashMap<String, SlotDefinition>>,
    pub presets: Vec<NodePreset>,
    pub default: Option<NodePreset>,
    pub locales: HashMap<…>,
    pub enabled_on: Option<Vec<String>>,
    pub disabled_on: Option<Vec<String>>,
}

ChildSpec deserializes from JSON strings: "slide"Named, "@theme"AnyTheme, "@app"AnyApp. @theme is expanded at load time to every registered theme node not already listed (section/registry.rs); @app is a stub, the marker is removed and no app nodes are injected.

SlotDefinition { accepts, max }, the per-slot child rules. Nodes without slots get one implicit default slot using accepts + max_children.

Page config: the map + order pattern

Persisted page/group configs (store_templates.config) use a map + order pattern at every depth:

pub struct PageConfig {
    pub sections: HashMap<String, NodeInstance>,  // identity in the map key
    pub order: Vec<String>,                       // display order
    pub wrapper: Option<String>,                  // preview/editor targeting
    pub layout: Option<String>,                   // optional layout override
}

pub struct NodeInstance {
    pub r#type: String,
    pub disabled: bool,
    pub settings: HashMap<String, serde_json::Value>,
    pub children: HashMap<String, NodeInstance>,  // old "blocks" key accepted
    pub child_order: Vec<String>,                 // old "block_order" accepted
    pub slot: String,                             // "default" unless named slot
}

The stable IDs in the map keys are what make drag-to-reorder, fragment hot-swap, and editor targeting work. TemplateConfig is an untagged enum over PageConfig (any page key) and ThemeSettingsConfig (the special _settings key: color_scheme, heading_font, body_font, plus a flattened catch-all extra map).

Resolution: config → resolved tree

resolve_node_config(page, theme) walks page.order[], looks up each instance’s schema in the theme, applies setting defaults, and resolves children recursively, skipping disabled nodes and warning (not failing) when a type no longer exists in the theme:

pub struct ResolvedNode {
    pub id: String,
    pub instance: NodeInstance,    // settings defaults merged in
    pub schema: NodeSchema,
    pub resolved_children: Vec<ResolvedNode>,
}

Rendering: the recursive node renderer

render_node(engine, slug, node, global, depth):

  1. Recursively renders all children, collecting each child’s rendered HTML into slots_html (keyed by the child’s slot) and into all_children (as NodeObjects, grandchildren included so render_node(child) can re-render nested templates inline).
  2. Builds the NodeObject for this node, children_rendered is the default slot’s HTML.
  3. Looks up {slug}/nodes/{type}.jinja and renders it with build_node_context: the storefront globals (optional ones stubbed) plus node.
  4. Wraps the result via wrap_node and returns (html, rendered_type_names), the type set drives CSS subsetting.

wrap_node

<!-- depth 0 (top-level) -->
<section id="rr-node-{id}" class="rr-node rr-node--hero rr-hero-wrapper">…</section>
<!-- depth > 0 adds editor hooks -->
<div id="rr-node-{id}" data-rr-node="{id}" data-rr-type="text-block" class="rr-node rr-node--text-block">…</div>

The class always includes rr-node rr-node--{type-slug} plus the schema’s class. node.attributes in templates emits the data-rr-node / data-rr-type pair so children rendered inside a parent’s markup (e.g. in a loop) stay editor-targetable.

The NodeObject template surface

Templates see node as a NodeObject (section/objects.rs): id, type, settings (a NodeSettingsObject over the JSON map), children (recursive NodeObject array), children_rendered, slots (per-slot HTML), slot, tag, class, attributes. JSON values convert through serde_to_minijinja, objects become traversable attribute-access wrappers, so node.settings.colors.primary works.

Section groups

section/groups.rs resolves and renders the header/footer groups: render_section_group(engine, slug, group_key, global) looks up the group’s preset from the manifest, resolves it, and renders every node, the same code path pages use. The API binary calls this for @root:header-group and @root:footer-group when the page template wants header_html / footer_html.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close