Nodes can ship their own CSS and JavaScript in dedicated blocks. The engine
extracts these at theme load into a per-theme ThemeAssetPipeline
(src/theme/loader.rs) and injects them into the rendered page.
@editor:section
<div class="rr-hero …">…</div>
{% stylesheet %}
.rr-hero { position: relative; overflow: hidden; }
{% endstylesheet %}
{% javascript %}
const bar = document.getElementById('$$node_id$$');
/* … */
{% endjavascript %}
{% schema %}
{ … }
{% endschema %}What happens at load time
For every .jinja file in layout/, nodes/, templates/, and
snippets/, the loader extracts the {% stylesheet %} and
{% javascript %} blocks and stores them under a style key:
- files in
layout/→ the reserved key__layout__; - everything else → the file stem (
hero.jinja→hero).
Then it computes combined_css / combined_js (all blocks concatenated in
sorted key order; JS blocks are each wrapped in an IIFE:
(function() { … })();). These combined strings back the engine’s
build_assets() for CDN-served combined.{hash}.css / .js artifacts.
How CSS reaches the page
The renderer tracks which node types were actually rendered on the page,
then asks the pipeline for a CSS subset (page_css):
__layout__CSS is always included first.- For each rendered type, its own stylesheet is included plus the
stylesheets of every child type in its
accepts/ slotaccepts(thestyle_ownershipmap). - Keys are deduplicated so a block is never emitted twice.
The subset is combined with the color-scheme CSS variables and injected
as one <style> in <head> via page_styles:
{{ page_styles | safe }}The result: a page with a hero + text_block renders only the CSS those
types (and their accepted children) own, not every stylesheet in the
theme.
How JS reaches the page
Node JavaScript blocks are rendered per node instance, with
$$node_id$$ replaced by the node’s DOM id, then each wrapped in an IIFE
and concatenated into page_scripts:
{{ page_scripts | safe }}The $$node_id$$ placeholder is how a node’s script scopes itself to its
own DOM subtree, the default theme uses it in announcement_bar,
navbar, navmenu, and slideshow:
{% javascript %}
const el = document.getElementById('$$node_id$$');
el.addEventListener('click', …);
{% endjavascript %}Node scripts are emitted after the theme’s global scripts and run inside
IIFEs, so they can’t leak top-level bindings between nodes. render_node
children that are re-rendered inline also re-emit their scripts through
this same path.
Templates and layout CSS
templates/*.jinja may also declare {% stylesheet %} blocks (404.jinja,
the account pages, cart.jinja via inline <style>). Because page
templates always render, their stylesheets join the subset through the
same mechanism (the whole template file’s style key is pulled in when the
template renders).
The duplication caveat
Themes README calls this out explicitly. CSS/JS blocks are emitted once per rendered type (CSS) or once per node instance (JS) by the page pipeline, but when a single node’s body renders its own markup twice (for example a navbar rendering the nav both as desktop links and inside a mobile panel), any inline styles or scripts inside the body markup duplicate in the output. The block-level extraction doesn’t dedupe content the body itself repeats.
This is a known, accepted limitation of the default theme today: “not
really a problem as of now.” Options when it becomes one: hoist shared
markup into a snippet (the default theme already does this for icons,
buttons, and inputs), or move per-node behavior into assets/main.js
keyed by data-* attributes, which is how the default theme handles most
interactivity.