Skip to content

Templates, syntax, globals, filters

What a Ringroad Jinja template can reference: the node object, globals, filters, and render_node.

Updated View as Markdown

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_url filter ({{ img.src | image_url }} in navbar.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 inside templates/.
  • {% include "default/snippets/layout/cart_drawer.jinja" ignore missing %} , whole-file includes (the default/ 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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close