Ringroad themes are written in a dialect of Jinja that extends the standard
Jinja2 syntax with four things vanilla Jinja does not understand. The
dialect compiles on MiniJinja (the Rust Jinja2 engine) only after the
extension blocks have been stripped by ringroad-renderer; a vanilla Jinja
processor, Python Jinja2, Tera, Liquid, etc., will crash on a Ringroad
theme file.
The four extensions
| # | Feature | Syntax | Purpose |
|---|---|---|---|
| 1 | Role marker | @editor:section / @editor:any on line 1 |
Tells the editor whether the node is a top-level section or a nested content atom |
| 2 | Schema block | {% schema %} … {% endschema %} |
JSON schema: settings, accepted children, slots, presets |
| 3 | Stylesheet block | {% stylesheet %} … {% endstylesheet %} |
CSS that ships with the node |
| 4 | JavaScript block | {% javascript %} … {% endjavascript %} |
JS that ships with the node |
A complete node file uses all four:
@editor:any
{# markup body, the node's HTML #}
<p class="rr-text">{{ node.settings.text | render_text | newlines_to_br }}</p>
{% stylesheet %}
.rr-text { color: var(--color-base-content); }
{% endstylesheet %}
{% schema %}
{
"name": "Text",
"role": "simple",
"settings": [ … ]
}
{% endschema %}Why vanilla processors crash
MiniJinja rejects {% schema %}, {% stylesheet %}, and {% javascript %}
as unknown statements. The engine’s theme loader strips those three
block types from the source (string search for {% tag %} … {% endtag %}
- see
strip_metadata_blocksinsrc/theme/loader.rs) before registering the template, and extracts the@editor:role marker separately. What MiniJinja actually compiles is the markup body only.
The stripped content isn’t lost, it becomes the editor schema and the asset pipeline:
flowchart LR
F[hero.jinja] --> X{loader pass}
X --> M[strip schema/stylesheet/javascript blocks]
X --> S["{% schema %} → NodeSchema in manifest"]
X --> C["{% stylesheet %} → asset pipeline CSS"]
X --> J["{% javascript %} → asset pipeline JS"]
M --> T[MiniJinja: default/nodes/hero.jinja]
What’s still vanilla Jinja
Everything else is ordinary Jinja2/MiniJinja, including the node body’s template logic:
- Expressions and filters:
{{ node.settings.heading }},{{ item.price | money }},{{ count | pluralize }} - Tags:
{% if %},{% for %},{% set %},{% block %},{% extends %},{% include %},{% from %} … {% import %},{% macro %} - Whitespace control and comment syntax
- Built-in filters:
safe,default,length,urlencode,lower,upper,replace,trim, math operators, etc.
Two runtime behaviors differ from Python Jinja2 and matter for theme authors:
- Lenient undefined, the engine sets
UndefinedBehavior::Lenient, so referencing a missing variable renders as an empty string instead of raising.{{ product.name }}on a page whereproductis absent renders ``, no crash, no conditional wrapper needed. Optional objects are additionally backed byStubObjectwrappers that return empty/falsefor any property. - Never-crash rendering,
render_nodeandrender_textswallow their own errors (empty string / original string returned), so a broken child or a malformed merchant-entered string degrades gracefully.
The runtime surface
Templates get a fixed set of globals, one per-node node object, and a
handful of custom filters/functions:
| Kind | Names |
|---|---|
| Globals | shop, settings, product, collection, collections, cart, page, search, blog, article, linklists, request, locale, extra, store |
| Node-local | node (settings, children, slots, attributes) |
| Filters | money, pluralize, newlines_to_br, render_text |
| Function | render_node(child) |
All of these are documented in Templates and Globals reference.
Styling & script caveat
CSS and JS in node blocks are emitted once per rendered type on a page, not once per node instance, so if a page renders the same node type twice, its stylesheet/script still appears once. However, if a node includes its own body twice (for example a navbar rendered in both desktop and mobile markup), the CSS/JS in the block can end up duplicated in the output. The default theme lives with this today; see Assets blocks for the details.