Skip to content

Theme directory structure

Every directory in a theme and the conventions each one follows.

Updated View as Markdown
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.js

Only 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:

  1. emits <title>, viewport, and font links (fonts are conditionally loaded based on the settings.heading_font / settings.body_font values via a font_stacks lookup map),
  2. links / _assets/main.css,
  3. emits an inline <style> block translating global settings into CSS custom properties (--color-*, --font-heading, --radius-*, --container-max, …),
  4. slots {{ settings.html_head | safe }} and {{ page_styles | safe }} into <head>,
  5. renders {{ header_html | safe }} (the header-group), a {% block main %} containing {{ page_content | safe }}, and {{ footer_html | safe }} (the footer-group),
  6. includes the cart drawer snippet and {{ page_scripts | safe }} before </body>, plus /_assets/main.js with defer.

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:section for top-level sections, @editor:any for 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 a NodeSchema and 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, and account/{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) and cart_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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close