Caching & Blitz
Caffeine is designed to sit behind a full-page cache. “Does it work with Blitz” hides three separate questions, and they have three separate answers.
The URL in the HTML goes stale
Cached HTML cannot contain /caffeine/products-v42.json, because v43 exists ten minutes later and the cached page still asks for v42.
So the page embeds a stable pointer — current.json — and never a versioned URL. The payload’s own filename is a hash of its contents, so it can be cached forever. That inverts the usual advice, deliberately: the small file is uncached and the large one is immutable.
The first paint depends on the query string
If the static cache key ignores the query string, a request for ?brand=acme gets HTML rendered for no refinements. Two ways to handle it.
Cache the canonical page only (recommended). The cached page is always the unrefined state. The runtime notices that the URL and the rendered state disagree and refines before paint, setting data-caffeine-hydrating on the wrapper while it does. One cache entry per listing, and the refinement costs one small request.
Or include the query string in the cache key. Every refinement is then server-rendered and cached — fastest for the visitor, but a listing with five facets has a combinatorial number of URLs and the cache fills with permutations nobody asked for twice.
Either way, emit a canonical URL so search engines index one page rather than forty thousand:
<link rel="canonical" href="{{ search.canonicalUrl }}">
No JavaScript at all
Every control is a real link or a real form. With the runtime absent they are ordinary page loads that server-render correctly, which is also why a crawler sees a working site.
What must not be cached
| Path | Why |
|---|---|
/caffeine/*/current.json | The pointer. Caching it is what makes a rebuild invisible. |
/caffeine/fragment | Depends entirely on the query string. |
Everything else under the publish path is content-addressed and safe to cache forever.
nginx
location /caffeine/ {
gzip_static on;
brotli_static on;
location ~ /current\.json$ {
add_header Cache-Control "no-cache";
}
location ~ \.(index|payload)\.json$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
}
A CDN in front is ideal and needs no special handling: the immutable files are the ones worth caching at the edge, and they already are.
Blitz
// config/blitz.php
return [
'cachingEnabled' => true,
// Cache the listing in its canonical, unrefined state.
// The runtime refines from there.
'queryStringCaching' => 0,
'excludedUriPatterns' => [
['siteId' => '', 'uriPattern' => 'caffeine/fragment'],
],
];
queryStringCaching => 0 is the important line: it tells Blitz to cache the URI without the query string, which is exactly the canonical state described above. If you would rather server-render refined states, use 2 and accept the cache-size trade.
Blitz’s own element tracking already refreshes the page when the underlying entries change, so nothing extra is needed. Its static file storage works unchanged too — the pointer lives under the publish path, not under Blitz’s cache, so nothing collides.
Craft’s template caching
Do not wrap a {% caffeine %} block in {% cache %}. It would cache the rendered state including whatever refinements happened to be in the first request that populated it — and there is nothing to gain, because the block is already rendered from an artifact rather than from element queries, which is the expensive thing {% cache %} exists to avoid.
Checking it works
Load the canonical URL, then load it again with ?brand=acme. The second should show refined results even though the HTML came from cache — that is the hydration path doing its job, and data-caffeine-hydrating on the wrapper is how you can see it happen.