Dynamic Template Values
Static caching works by writing a page to disk once and serving those same bytes to everyone, without booting ExpressionEngine. That is what makes it fast, and it is also why anything that must differ per request — a CSRF token, a CSP nonce — cannot simply be baked into the cached file.
Speedy solves this with a two-sided placeholder swap:
On write, the live value is stripped out of the rendered page and replaced with a placeholder such as
{csp_nonce}, so the real value never touches disk.On read, every time the cached page is served, a fresh value is generated and substituted back in — along with any HTTP headers that value feeds.
Speedy has always done this for csrf_token. As of this release the mechanism is configurable, so you can add your own per-request values.
Quick start: a CSP nonce
$config['speedy_dynamic_values'] = [
'csp_nonce' => [
'generate' => 'random_base64:16',
'capture' => ['/nonce="[A-Za-z0-9+\/=]{16,}"/um' => 'nonce="{csp_nonce}"'],
'headers' => ["Content-Security-Policy: script-src 'nonce-%s' 'strict-dynamic'"],
],
];In your template, output the nonce as you normally would for an uncached page. Speedy's capture pattern rewrites it to a placeholder on the way into the cache:
<script nonce="{your_nonce_variable}">…</script>Every cached hit then gets a fresh nonce in both the markup and the Content-Security-Policy header, and the two always match.
Read Caching and per-request values before deploying a CSP nonce. A per-request nonce is incompatible with a shared CDN cache in front of your origin.
Configuration reference
Each entry is keyed by its placeholder name. The name must match [a-z0-9_]{1,64} and becomes the {placeholder} token used in your content.
capture
no
Write-side patterns that strip a live value back to a placeholder.
replace
no
Read-side substitutions. Defaults to replacing the bare {name} token.
headers
no
Response headers to send, each a sprintf template receiving the value.
Generators
random_base64:N
byte length (required)
Base64 of N random bytes. random_base64:16 → pEtSB2wbOHy+13LcHh/7Yw==
random_hex:N
byte length (required)
Hex of N random bytes. random_hex:8 → 9f3a1c72b40de815
uuid4
—
A version 4 UUID.
timestamp
—
The current Unix timestamp, as a string.
csrf_token
—
The current user's ExpressionEngine CSRF token.
Anything not in this list is rejected. There is deliberately no way to supply arbitrary PHP: generator names are compiled into the static cache files Speedy writes, so keeping the set closed means a typo produces a clear error in the control panel rather than broken pages.
capture — keeping live values off disk
capture runs when a page is cached. It is a map of regular expression to replacement, applied to the rendered HTML before it is written:
You can also give a single pattern as a plain string, or a list of patterns. In both cases the replacement defaults to the entry's own {placeholder} token:
capture is optional. If your template outputs the literal placeholder text {csp_nonce} rather than a real value, there is nothing to strip and you can omit it entirely. That is the simpler setup, but note the uncached first request will then contain the literal placeholder too — so it only works if the page is never served uncached, which is rarely true. Most sites want capture.
replace — putting the value back
replace runs every time a cached page is served. It maps a literal search string to a sprintf template that receives the generated value:
The default — replacing the bare {name} token with the raw value — is what you want most of the time, so replace is usually omitted. Specify it when one value needs to appear in several different shapes; the built-in csrf_token entry does exactly this to handle links, form fields, and the {speedy_csrf_token} variable.
Substitution uses plain string replacement, never a regex, so a generated value containing $1 or \0 is inserted literally.
headers
Each header is a sprintf template receiving the generated value:
The value generated for the header is the same value substituted into the markup on that request — that is what makes a CSP nonce work. Headers containing a line break are rejected, to prevent one entry from injecting others.
Headers are only sent if the value is non-empty, and only if the page has not already started output.
The built-in csrf_token entry
Speedy ships this entry by default. You do not need to configure it, and the behaviour is identical to previous releases:
Overriding it
Defining your own csrf_token key replaces Speedy's entry entirely — the two are not merged, so that the result is predictable. Copy the block above and edit it rather than writing a partial entry.
A common reason to override is a form field Speedy's default patterns do not match, for example one rendered with single quotes:
Disabling it
Set the entry to false:
Only do this if your site has no forms served from cached pages. With CSRF disabled, Speedy skips building its CSRF machinery on cached requests entirely, which avoids a database lookup — but any form on a cached page will submit an unusable token.
More examples
Request correlation
Emit an identifier into the page so a CDN log line can be matched to an origin log line:
Echoing an A/B bucket
If a cookie assigns visitors to a test bucket, a cached page can reflect it:
A nonce plus a request id together
Entries are independent and can be combined freely:
Caching and per-request values
A per-request value cannot survive a shared cache. If a CDN, reverse proxy, or Varnish instance sits in front of your origin and caches the page publicly, every visitor receives the same stored copy — including the same nonce. A CSP nonce reused across visitors provides no protection.
If you use Speedy's reverse-proxy purgers (Cloudflare, Fastly, CloudFront) with a public cache-control, dynamic values will not behave correctly for pages cached at the edge. Dynamic values are for pages served from your origin's static cache, where PHP still runs on each request.
The same caveat applies to any server rule that serves Speedy's cached .html files directly. Speedy writes both an index.php and (with speedy_static_use_html_file) an index.html; substitution happens in the PHP file. An Nginx try_files rule that serves the .html and bypasses PHP will serve raw, unsubstituted placeholders.
Changing configuration does not rewrite existing cache files
Each static cache file embeds a snapshot of the definitions in force when it was written. Editing speedy_dynamic_values affects pages cached after the change; already-cached pages keep their old behaviour until they expire or are cleared.
After changing this setting, clear your cache. Existing files continue to work — they are never left broken — but a newly added placeholder will not be substituted in a page cached before you added it.
Cache files written by a version of Speedy predating this feature still have their CSRF tokens replaced correctly; they fall back to the built-in behaviour.
Regenerate static utility files
This feature adds a file to your static cache's utilities/ directory. After upgrading, go to Speedy's control panel and use Regenerate files if prompted. Until you do, dynamic values are skipped rather than applied — pages still serve correctly, but new placeholders are not substituted.
Safety notes
Cookie values are filtered
Cookies are attacker-controlled. A value from cookie: is only used if it matches [A-Za-z0-9_\-.:] — anything else becomes an empty string rather than being written into your page. This prevents a crafted cookie from breaking out of an HTML attribute or injecting a response header.
Even so, treat a cookie-derived value as untrusted input, and do not place it anywhere a partial value could change the meaning of surrounding markup.
Invalid configuration is reported, not fatal
An entry that fails validation is skipped and an error is shown in Speedy's control panel. Other entries continue to work. Validation rejects unknown generators, missing or malformed generator arguments, zero-length random values, invalid placeholder names, malformed regular expressions, and headers containing line breaks.
If a placeholder is never being replaced, check the control panel first.
config.php is trusted
speedy_dynamic_values is read from config.php, which is already trusted PHP. Capture patterns are compiled into generated cache files, so treat this setting with the same care as the rest of that file.
Troubleshooting
The placeholder appears verbatim on the page. The read side did not run. Check that you have regenerated the static utility files, that the page is being served through index.php rather than a direct .html rule, and that the control panel shows no configuration errors.
The live value is on disk instead of a placeholder. Your capture pattern is not matching the rendered output. View the cached file under your static path and compare it to the pattern. Remember that capture runs against the fully rendered HTML, after EE has parsed the template.
Every visitor gets the same nonce. A shared cache is serving one stored copy. See Caching and per-request values.
Forms started failing CSRF validation after overriding csrf_token. An override replaces the default entirely. Make sure your capture and replace lists together cover every place a token appears — links, hidden fields, and any JavaScript that reads one.
Last updated