UI RENDERING & SSR FEATURE
Script Injection
Deferred blocks carry their own JS — injected once when the block arrives, never duplicated.
Feature Guide
A quick orientation block that answers the essential questions: what this feature does, how it works, why it matters, and the key concepts behind it.
What this does
Deferred blocks carry their own JS — injected once when the block arrives, never duplicated.
Client Modules
Inject block JS exactly once
Each deferred slot declares its client modules in the attribute, and the framework de-duplicates them across the page.
#[AsSlotResource(
handle: 'demo_deferred_scripts',
slot: 'deferred_chart_widget',
template: '...chart-widget.html.twig',
deferred: true,
clientModules: ['@project-static-semitexa-demo/deferred/chart-widget.js'],
)]
The module initializes only after semitexa:block:rendered fires for the delivered block.
Verified against Semitexa Ultimate 2026.09.19.1020
Script Injection
Deferred slots can declare client modules that are injected once when the block lands. Deferred blocks carry their own JS — injected once when the block arrives, never duplicated.
How it works
The clientModules key in #[AsSlotResource] declares the JavaScript module paths the deferred block needs. When the block HTML arrives over SSE, the framework injects those modules into the page exactly once. The module initializes only after semitexa:block:rendered fires for the delivered block.
#[AsSlotResource(
handle: 'demo_deferred_scripts',
slot: 'deferred_chart_widget',
template: '...chart-widget.html.twig',
deferred: true,
clientModules: ['@project-static-semitexa-demo/deferred/chart-widget.js'],
)]
Key mechanisms
clientModules— declares the JS module paths the block needs, directly in the slot attribute.semitexa:block:rendered— browser event fired after the block HTML has been inserted into the DOM.- auto-play — the framework handles deduplication, so the same module is not loaded twice even if the block appears multiple times.
- script isolation — each block's scripts activate after its own
renderedevent, keeping timing scoped to the correct block.
Not an inline <script>
Writing the script inside the block's own template is the obvious thing to do, and it works — until the block starts arriving over SSE. Markup that arrives in a live document changes what a script tag means, in three ways that nothing announces:
- It is inert. A script parsed out of a fragment and inserted does not run. It has to be re-created — and re-created with this document's CSP nonce, not the one it was parsed with, which under a strict
script-srcis the difference between running and being blocked on every arrival. A re-creation that carries the current nonce is fine; one that omits it, or copies the nonce from the response the fragment came from, is refused while the page otherwise looks like it works. - It runs more than once. Every arrival re-creates it, so every binding it makes has to be idempotent. A listener attached to
documentaccumulates silently, once per delivery. DOMContentLoadedhas already fired. Code waiting for it never runs. A field-autocomplete partial that bound on that event simply stopped binding for any block that arrived by SSE.
Two mechanisms avoid all three, and lint:deferred-twig reports an inline <script> in a deferred template naming them (that is the template-compatibility lint; the similarly named lint:deferred-slots is a different check — it compares a slot's deferred: true against whether any page actually defers it):
clientModulesfor code that belongs to this block — a module served from your own origin, injected once, initialized onsemitexa:block:rendered.#[AsUiBehavior]for an interaction with no server state — a dropdown, a modal, tabs, a tooltip. The behavior runtime watches the document with a singleMutationObserverand connects late-arriving markup wherever it appears, so timing stops being your problem.
make:page --with-assets scaffolds the module shape rather than an inline block for the same reason.
Why this matters
Without a structured mechanism, deferred blocks that need JavaScript require either page-level script includes (which load unconditionally) or runtime dynamic imports that need custom timing logic. Declaring clientModules on the slot attribute keeps the dependency co-located and the injection lifecycle automatic.