The markup body of a .jinja file is ordinary Jinja2 syntax compiled by
MiniJinja with UndefinedBehavior::Lenient. This page covers the
Ringroad-specific runtime surface: the node object, the globals, the
custom filters, and render_node.
The node object
Every node template receives a node variable, a NodeObject
(src/section/objects.rs) describing the node instance at any depth:
| Property | Type | Example |
|---|---|---|
node.id |
string | The stable instance ID (UUID from the page config) |
node.type |
string | Node type ("hero", "text_block") |
node.settings |
object | The node’s settings, defaults merged (node.settings.heading) |
node.children |
array | Ordered child node objects, loop and inspect, or pass to render_node(child) |
node.children_rendered |
string | Pre-rendered HTML of all default-slot children |
node.slots |
object | Pre-rendered HTML per named slot (node.slots.cta) |
node.slot |
string | The slot this node was placed into ("default", "col", …) |
node.tag |
string | Wrapper tag from the schema ("section", "div") |
node.class |
string? | Extra wrapper class from the schema |
node.attributes |
string | data-rr-node="{id}" data-rr-type="{type}" for editor targeting |
Settings values are JSON values converted for template access: strings,
numbers, booleans, nested objects (node.settings.colors.primary), and
arrays.
{% if node.settings.heading %}
<h1 class="rr-hero__heading">{{ node.settings.heading }}</h1>
{% endif %}Because the renderer applies schema defaults before rendering, every declared setting resolves even before the merchant touches the settings panel.
Globals
The context merges request-level storefront globals with the node-local
node. The full reference is in Globals; the quick
map:
| Global | Contents |
|---|---|
shop |
Store profile: name, domain, currency_code, currency_symbol, logo_url, … |
store |
The store slug string |
settings |
Global theme settings: settings.colors.primary, settings.heading_font, settings.html_head, … |
product |
Primary product on product pages (undefined elsewhere) |
collection |
Primary collection on collection pages |
collections |
Nav collections, keyed by handle: collections.summer.title |
cart |
Cart: items, item_count, total_price, currency_code |
page |
CMS page: handle, title, content_html |
search |
query, results_count |
blog / article |
Blog and article objects |
linklists |
Menus keyed by handle: linklists["main-menu"].items |
request |
path, page_type |
locale |
code (stub) |
extra |
Route-handler data bag: extra.listing, extra.sort, extra.collections, … |
Template pages additionally receive header_html, page_content,
footer_html, page_styles, and page_scripts from the renderer.
Lenient undefined means optional globals (product on an index page)
render empty without guards, but StubObject wrappers make even chained
access (product.description) return empty/falsy instead of erroring.
Filters
The engine registers four custom filters (src/engine/filters.rs):
money
Formats an integer amount in minor units as a currency string. Takes an
optional currency argument (ISO code; default "USD").
{{ product.base_price | money }} {# $19.99 #}
{{ 129900 | money(currency="NGN") }} {# ₦1,299.00 #}
{{ (item.price * item.quantity) | money }} {# arithmetic first #}Uses ringroad_constants::currency for the exponent and symbol; negative
amounts render with a leading -.
pluralize
{{ cart.item_count | pluralize(singular="item", plural="items") }}Returns the singular form when the value is 1, plural otherwise
(defaults "item" / "items").
newlines_to_br
Replaces \n with <br>.
<p>{{ node.settings.subtext | newlines_to_br }}</p>render_text
Renders a merchant-entered string as a nested MiniJinja template with the current context, so text settings can interpolate variables:
{{ node.settings.text | render_text }}
{# with text = "Hello {{ product.name }}" → "Hello <product name>" #}Never raises, on any error the original string is returned unchanged.
Built-ins you’ll use constantly
| safe (raw HTML, page.content_html, page_styles, header_html),
| default("…"), | length, | urlencode, | lower / | upper,
| replace, | trim, | join, | first / | last.
Known gap: the default theme calls an
image_urlfilter ({{ img.src | image_url }}innavbar.jinja,product_gallery.jinja,product_recommendations.jinja) that is not registered in the engine. MiniJinja raises an unknown-filter error for those render paths today.
render_node(child), inline child rendering
A global function that renders a child node’s own template inline, with
the current scope, the counterpart to node.children_rendered when the
parent needs loop variables (e.g. product inside a {% for %}) visible
to the child:
{% for child in node.children %}
{{ render_node(child) }}
{% endfor %}The output is wrapped in the same rr-node-{id} wrapper the storefront
renderer produces (including data-rr-node / data-rr-type), so children
rendered inline remain targetable in the preview editor. The theme slug is
derived from the current template name. Never raises: unknown type or
missing template → empty string.
featured_products.jinja uses it to render the same product-card children
once per product:
{% for product in extra.featured_products %}
{% for child in node.children %}
{{ render_node(child) }}
{% endfor %}
{% endfor %}Composition
Templates compose like standard Jinja:
{% extends "default/layout/theme.jinja" %}+{% block main %}, page templates insidetemplates/.{% include "default/snippets/layout/cart_drawer.jinja" ignore missing %}, whole-file includes (thedefault/prefix is the theme slug).{% from "default/snippets/ui/button.jinja" import button %}then{{ button(label="Shop now", variant="primary") }}, macros.{% set %}/{% if %}/{% for %}, ordinary control flow, used heavily in the layout for font and radius lookup maps.
Because ignore missing is supported, optional snippets degrade silently.