Everything a developer changes on a Core dna site lives in its theme repository. This guide covers the repository and its build, printing records in templates, stylesheets and client-side code, and the parts of a site around the main content.
L1–L2
Unit 01 · Anatomy of a theme repo
Queueing a script from a template
Anatomy of a theme repoPage 6 of 35Updated
A component that needs a bundle should add it to a queue, and the base template should print every queued script once, at the end of the page, with the nonce and the cache-buster. The queue in both engines, the block alternative and where it stops working, and queueing code that waits for a bundle.
Who this is for. The theme developer writing a component or include that needs JavaScript from a bundle the base template does not load. Afterwards you can load that bundle exactly once, on exactly the pages that render the component, without writing a <script> tag in the middle of the page.
Why a queue instead of a script tag
A <script> printed by a component appears wherever the component renders, once per use: a form component used twice loads the form bundle twice, and runs it before the libraries the base template loads at the end of the body. Each copy is also one more tag that must remember the nonce and the cache-buster. A queue keeps ordering, de-duplication, the nonce and the cache-buster in one place, the base template.
The platform has no script helper. The store is _templateContext, an object shared by every template rendered in the same request, with get(key, default) and set(key, value). It is a common variable, so it is available in components, includes and records rendered inside the page.
The queue
In the component, add the bundle's URL unless it is already queued. At the end of the base template, print the queue:
Template codecomponents/form.html and the base template
{* in the component or include that needs the bundle *}
{$scripts = $_templateContext->get('deferredScripts', [])}
{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', []) as $src}
<script src="{$src}?v={$_currentSite->getLastVcsHash()}" nonce="{$_inlineScriptNonce}" defer></script>
{/foreach}
{# in the component or include that needs the bundle #}
{% 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 src="{{ src }}?v={{ _currentSite.getLastVcsHash() }}" nonce="{{ _inlineScriptNonce }}" defer></script>
{% endfor %}
One production Smarty site keeps two queues this way, scripts printed with async and deferredScripts printed with defer, and its form include queues the platform's form scripts and the site's form bundle in one go. Pick names once and write them in the base template's header comment, because a typo in a key queues into a list nothing prints.
The queue must be full before it is printed
Templates render top to bottom, and blocks render where the base template places them, in both engines. A component inside the page's content block has run by the time the base template reaches the end of the body, so its entry is printed. Two things are not:
An entry added after the loop, for example by a footer include placed below it. Put the loop last.
An entry added in another request, such as a fragment fetched with JavaScript. That request has its own context; the page that loaded the fragment never sees its queue. Load the bundle from the page, or from the script that fetches the fragment.
The block alternative, and its limit
A template in the inheritance chain can add to a block in the base template instead:
Template code
{* base template *}
{block name="scripts"}{/block}
{* a page template that extends it *}
{block name="scripts" append}
<script src="/dist/checkout.js?v={$_currentSite->getLastVcsHash()}" nonce="{$_inlineScriptNonce}" defer></script>
{/block}
{# base template #}
{% block scripts %}{% endblock %}
{# a page template that extends it #}
{% block scripts %}
{{ parent() }}
<script src="/dist/checkout.js?v={{ _currentSite.getLastVcsHash() }}" nonce="{{ _inlineScriptNonce }}" defer></script>
{% endblock %}
That works for a page template, which extends the base. It does not work from an include or a component, which extends nothing and cannot reach the base template's blocks. That is the case the queue exists for.
Inline code that waits for a bundle
Sometimes a template needs to run a few lines with data only it has, using code from a deferred bundle that has not loaded yet. Queue a function rather than a URL: define a list early in the <head>, push functions onto it from templates, and have the bundle run them when it loads and run later ones at once.
Template code
{* in the <head> of the base template *}
<script nonce="{$_inlineScriptNonce}">window.onBundleReady = window.onBundleReady || [];</script>
{* in any template *}
<script nonce="{$_inlineScriptNonce}">
window.onBundleReady.push(function () {
window.site.initMap({$location|json_encode});
});
</script>
{# in the <head> of the base template #}
<script nonce="{{ _inlineScriptNonce }}">window.onBundleReady = window.onBundleReady || [];</script>
{# in any template #}
<script nonce="{{ _inlineScriptNonce }}">
window.onBundleReady.push(function () {
window.site.initMap({{ location|json_encode }});
});
</script>
JavaScriptsrc/js/main.js
// at the end of the bundle's entry file
const queued = window.onBundleReady || [];
window.onBundleReady = { push: (fn) => fn() };
queued.forEach((fn) => fn());
Every inline script in that pattern carries the nonce, or the browser refuses it without an error on the page: The CSP nonce. Values printed into a script go through json_encode, never into a quoted string by hand: Escaping output.