A Ringroad theme is a directory of Ringroad Jinja files plus a
theme.json manifest. It is server-side rendered (SSR) and sectioned: a
merchant composes pages section by section in the theme editor instead of
hand-editing templates.
The five parts of a theme
themes/{slug}/
├── theme.json # manifest: global settings, page presets, section groups
├── layout/ # the page shell (theme.jinja)
├── nodes/ # every node type, sections AND content atoms
├── templates/ # whole-page templates (cart, checkout, 404, account/*)
├── snippets/ # reusable Jinja macros (buttons, inputs, icons, …)
└── assets/ # static main.css / main.js served as /_assets/*The directory name becomes the theme slug (default for
themes/default/). A store picks one active theme; the API reads it from
stores.current_theme and builds a rendering engine over the whole
themes/ directory at startup.
Nodes are the unit of editing
The core idea of the theme system: everything a merchant edits is a node. There is no longer a hard distinction between a “section” and a “block”, both are nodes with different roles:
| Role | Marker | Editor placement |
|---|---|---|
section |
@editor:section at the top of the file |
Top-level: “Add section” panel, page or section group |
simple |
@editor:any |
Nested inside another node: “Add child” panel |
themes/default/nodes/ has 45 node types: layout containers (hero,
columns, rows, slideshow), navigation (navbar, navlink,
navmenu), commerce listings (catalog_products, featured_products,
collection_products), product-detail atoms (product_title,
product_price, buy_buttons, variant_picker), and marketing/footer
(newsletter, testimonials, footer).
Every node file declares three things in one place:
- Markup, the Jinja body that renders the node,
{% schema %}, JSON describing settings, accepted children, slots, and presets for the editor,{% stylesheet %}/{% javascript %}, CSS and JS scoped to the node (optional).
How a page renders
flowchart LR R[Request] --> E[Engine::render_page] E --> L[load_config: page tree] E --> H[header-group nodes] E --> B[page nodes] E --> F[footer-group nodes] H --> N[render_node × n] B --> N F --> N N --> C[CSS subset + vars] N --> S[JS blocks] E --> T[layout/theme.jinja] T --> OUT[Rendered HTML]
- The API route handler resolves the store’s theme and builds a
StorefrontContext(shop, product, cart, collections, …) and anextra.*bag of page data. Engine::render_page()loads the page’s node tree, persisted merchant edits fromstore_templates.config, falling back to the theme preset, plus the shared header-group and footer-group node trees.- Each node renders recursively (children inside parents), settings merged with schema defaults.
- Rendered node types drive CSS subsetting: only the stylesheet blocks
of the types actually present on the page (plus their accepted children)
are inlined as
page_styles; JavaScript blocks are concatenated aspage_scripts. - The page is wrapped by
layout/theme.jinja, which emits the design tokens from the global settings as CSS custom properties and slots the header, content, footer, styles, and scripts into the shell.
The editor preview is the same pipeline with is_preview=true: it injects
extra preview CSS/JS and can hot-swap a single node’s HTML via
Engine::render_node_fragment().
Themes, stores, and persistence
- Themes live in the repo under
themes/and are loaded at engine startup. The engine is database-free; all data flows in through theDataSourcestrait. - Merchant edits persist in
store_templatesas JSONconfigperpage_key:_settings(global settings),@root:header-group,@root:footer-group, and page keys likeindex,product,cart. - The editor (storeadmin) writes the same
PageConfigshape the renderer reads: a map of node instances keyed by stable UUID-like IDs plus anorderarray (see Page configs).