themes/{slug}/
├── theme.json
├── layout/
│ └── theme.jinja # the page shell (required for real themes)
├── nodes/
│ ├── hero.jinja # one file per node type; name == type
│ ├── text_block.jinja
│ └── …
├── templates/
│ ├── index.jinja # page templates, one per page key
│ ├── cart.jinja
│ ├── checkout.jinja
│ ├── 404.jinja
│ └── account/ # nested page templates
├── snippets/
│ ├── ui/ # generic macros: icon, button, input, …
│ ├── product/ # product_card, product_grid, add_to_cart
│ └── layout/ # search_bar, cart_drawer
└── assets/
├── main.css # served at /_assets/main.css
└── main.js # served at /_assets/main.jsOnly four directories are walked and registered with the template engine:
layout/, nodes/, templates/, and snippets/. Everything else
(assets/, theme.json, …) is handled separately.
Template naming: {slug}/{dir}/{relpath}
Every .jinja file is registered with MiniJinja under a namespaced name:
the theme slug, the directory, then the relative path (subdirectories
allowed). This is how templates reference each other:
{% extends "default/layout/theme.jinja" %}
{% include "default/snippets/layout/cart_drawer.jinja" ignore missing %}
{% from "default/snippets/ui/icon.jinja" import icon %}Node bodies are looked up dynamically by type:
{slug}/nodes/{node_type}.jinja (see
render_node).
layout/, the page shell
One file today: theme.jinja. It is the <!doctype html> document that
every page renders inside. The shell:
- emits
<title>, viewport, and font links (fonts are conditionally loaded based on thesettings.heading_font/settings.body_fontvalues via afont_stackslookup map), - links
/ _assets/main.css, - emits an inline
<style>block translating global settings into CSS custom properties (--color-*,--font-heading,--radius-*,--container-max, …), - slots
{{ settings.html_head | safe }}and{{ page_styles | safe }}into<head>, - renders
{{ header_html | safe }}(the header-group), a{% block main %}containing{{ page_content | safe }}, and{{ footer_html | safe }}(the footer-group), - includes the cart drawer snippet and
{{ page_scripts | safe }}before</body>, plus/_assets/main.jswithdefer.
Page templates {% extends %} this shell and override {% block main %}.
nodes/, one file per node type
The filename (minus .jinja) is the node type. hero.jinja → type
hero; a node instance in a page config with "type": "hero" renders
default/nodes/hero.jinja. Conventions:
- Start the file with a role marker on the first line:
@editor:sectionfor top-level sections,@editor:anyfor content atoms. - Order the file: body markup, then
{% stylesheet %}(optional), then{% javascript %}(optional), then{% schema %}(required for editor-manageable nodes). The default theme uses exactly this order. - Files are read at startup: the
{% schema %}block is parsed into aNodeSchemaand registered in the manifest keyed by type; the stylesheet/javascript blocks are extracted into the asset pipeline; the rest (markup) is registered with MiniJinja.
One exception: collection_filters.jinja in the default theme has no
{% schema %} at all, it is a developer-managed node not exposed to the
editor.
templates/, whole-page templates
One file per page key: {page_key}.jinja. When a page key has a template,
it wins over section composition (see
Template resolution).
The default theme’s pattern:
- Stub templates (102 bytes):
index,product,collection,catalog,search,collections,contact, these just{% extends %}the layout and dump{{ page_content | safe }}into the main block. All real content comes from the page’s preset/saved node tree. - Real templates:
cart.jinja,checkout.jinja,404.jinja, andaccount/{login,register,forgot_password,reset_password,dashboard,orders,order}.jinja, bespoke pages that override{% block main %}with hand-written markup and client-rendered sections.
Templates may contain {% stylesheet %} blocks too (404.jinja,
account/* do); the layout handles those via page_styles.
snippets/, reusable macros
Jinja macros, imported by name from the namespaced path. The default theme groups them:
ui/, generic atoms:icon(name, size, class)(an inline SVG set),button(label, variant, size, …),input(...),textarea(...),qty_stepper(...),price(...),badge(...),empty_state(...),section_heading(...),breadcrumbs(items),pagination(pg, base),listing_toolbar(total, sort, action, …).product/,product_card(product),product_grid(products, columns),add_to_cart(product).layout/,search_bar(...)(macro) andcart_drawer.jinja(include- only, not a macro).
Snippets are pure: they render markup from their arguments and never reach into request state, so they work from any context.
assets/, static files
main.css and main.js are the theme’s global assets, served at
/_assets/main.css and /_assets/main.js. The default theme’s assets are
token-driven: all colors and radii come from the var(--color-*) /
var(--radius-*) custom properties the layout emits, and all JS behavior
is wired through data-* attributes (cart drawer, qty steppers, gallery,
variant picker, announcement bar).
Per-node CSS/JS belongs in the node file’s {% stylesheet %} /
{% javascript %} blocks, not in assets/.
Editor markup conventions
The renderer wraps every node in a semantic tag with editor-targeting attributes (see wrap_node):
- Top-level nodes:
<section id="rr-node-{id}" class="rr-node rr-node--{type}"> - Nested nodes add
data-rr-node="{id}" data-rr-type="{type}"so templates can emit{{ node.attributes }}for editor-targetable children rendered outside the wrapper (e.g. inside a loop).
Themes should not fight these wrappers: style with rr-node--{type} and
the node’s own class from its schema rather than relying on element
position.