How-to library · Developer docs

How-to library

Task-shaped pages: a title that starts with a verb, the situation in a few lines, the steps, the working code in both engines, and why this way rather than another. Found from a search, done, and left.

Register a page-specific JS bundle with a CSP nonce

Build a separate entry, queue its URL from the template or component that needs it through _templateContext, and let the base template print every queued script once, with the nonce and the deploy hash.

Context

Context. A checkout, a course player or a form engine is real weight that a content page should not download. Build it as its own bundle, and load it only where it is used. Two rules apply to every script tag on the platform: it carries the nonce, or the Content Security Policy refuses it with no mark on the page, and its URL carries the deploy hash, or returning visitors keep the old file. A component should not print its own script tag mid-page; it queues the URL, and the base template prints the queue in one place.

Steps

  1. Add an entry to the build (for example src/js/forms.js producing dist/forms.js), build, and commit the output.
  2. In the template or component that needs it, add the URL to a queue in _templateContext, checking first so a component used twice queues once.
  3. At the end of the base template, print every queued URL with the nonce, the hash and defer.
  4. Verify in the browser console after deploying: a refused script is reported there and nowhere else.

Template code

Template code
{* in the component or page template *}
{$scripts = $_templateContext->get('deferredScripts')|default:[]}
{if !in_array('/dist/forms.js', $scripts)}
  {$scripts[] = '/dist/forms.js'}
  {$_templateContext->set('deferredScripts', $scripts)}
{/if}

{* at the end of the base template, before </body> *}
{foreach $_templateContext->get('deferredScripts')|default:[] as $src}
  <script nonce="{$_inlineScriptNonce}" src="{$src}?v={$_currentSite->getLastVcsHash()}" defer></script>
{/foreach}
{# in the component or page template #}
{% set scripts = _templateContext.get('deferredScripts') ?? [] %}
{% if '/dist/forms.js' not in scripts %}
  {% do _templateContext.set('deferredScripts', scripts|merge(['/dist/forms.js'])) %}
{% endif %}

{# at the end of the base template, before </body> #}
{% for src in _templateContext.get('deferredScripts') ?? [] %}
  <script nonce="{{ _inlineScriptNonce }}" src="{{ src }}?v={{ _currentSite.lastVcsHash }}" defer></script>
{% endfor %}

Why this and not that

Not a script tag inside the component: it would print once per instance, without the nonce half the time, and before the shared bundle it depends on. Not a {block} in the inheritance chain: a block only works from templates in the {extends} chain, and a component or include is not in it; the context queue works from anywhere in the render. Not one bundle for everything: the rule of thumb is a bundle per page type when the code is heavy, not per feature.

Go deeper

Was this page helpful?

Your answer goes to the docs team.