Skip to content

{% stylesheet %} / {% javascript %}, asset blocks

Scoped CSS and JS inside node files, and how the engine subsets, combines, and injects them.

Updated View as Markdown

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.jinjahero).

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

  1. __layout__ CSS is always included first.
  2. For each rendered type, its own stylesheet is included plus the stylesheets of every child type in its accepts / slot accepts (the style_ownership map).
  3. 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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close