Decide where the pages of a Core dna site are composed before writing code: on the platform, in your own application, or on the platform with a few pieces filled in by the browser. The decision tree, each shape in depth, multi-brand and languages, and a table and case studies to make the call.
L2
Unit 04 · Hybrid, the common case
Dynamic fragments
Hybrid, the common casePage 16 of 29Updated
The pattern that keeps a personal page cacheable: a fragment template that switches caching off, a page layout that serves fragments by name, and a script that fetches them after the cached page has painted. The whole mechanism, in both engines, and how to check it works.
Who this is for. The theme developer and the JavaScript developer of any site with members, a cart or personal content, which is most of them. This page owns the pattern; other guides link here. After it you can take a personal element out of a cached page and deliver it on its own.
Why fragments
The platform stores pages rendered for visitors who are not signed in, and sends the stored copy before it has checked who is asking. Anything personal inside a cacheable page is therefore either the wrong visitor's, or the signed-out version shown to a member (What one personal element does). Switching the cache off for the whole page fixes that at the price of rendering every view from scratch.
A fragment is the alternative: the personal part is rendered on its own, never cached, and fetched by the browser after the cached page has painted. The page stays in the cache for everyone; only the fragment is rendered per visitor, and it is small.
The mechanism
A page layout called, say, Fragment, whose template serves fragment templates by name.
A page for each fragment. In the admin, a parent page such as /fragments, with a child page on the Fragment layout for each fragment: account, cart-count. Its slug is the fragment's name. Unpublishing the child switches the fragment off. Set each one's robots setting to Don't index and don't follow, which also keeps it out of the generated sitemap.
A fragment template for each, which switches caching off and renders only the personal part.
A placeholder in the cached page and a script that replaces it with the fragment.
The dispatcher
The Fragment layout's template looks the page's slug up in a list of known fragments and includes the one it names. The list matters in Smarty, where including a file that does not exist stops the page with an error, and it documents in one place which fragments the site has:
{% do _response.preventCaching() %}
{% set fragments = {'account': 'fragments/account.html', 'cart-count': 'fragments/cart-count.html'} %}
{% set name = page.getSlug() %}
{% if fragments[name] is defined %}
{{ include(fragments[name]) }}
{% endif %}
The fragment template
A fragment renders only the personal part, with no layout around it, and calls preventCaching() itself as well, so that it stays uncached whichever page includes it. The account fragment also carries the session's CSRF token for the site's scripts, as a data attribute, because a script inserted into a page with innerHTML does not run:
The cached page holds a placeholder with the signed-out version inside it, so the page is complete before the script runs and without JavaScript at all. It names the fragment it wants:
import { setCsrfToken } from './api-client.js';
// Replace every placeholder with its fragment, after the cached page has painted.
export async function loadFragments() {
const slots = document.querySelectorAll('[data-fragment]');
await Promise.all([...slots].map(async (slot) => {
const response = await fetch(slot.dataset.fragment, { credentials: 'same-origin' });
if (!response.ok) return; // keep the placeholder: the page still works
const holder = document.createElement('div');
holder.innerHTML = await response.text();
const token = holder.querySelector('[data-csrf]')?.dataset.csrf;
if (token) setCsrfToken(token);
slot.replaceChildren(...holder.childNodes);
slot.dispatchEvent(new CustomEvent('fragment:loaded', { bubbles: true }));
}));
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadFragments);
} else {
loadFragments();
}
The token goes to the one module that sends requests (One client module), and the event lets other scripts wait for the fragment instead of racing it. The swap is invisible to most visitors, because most are not signed in and the placeholder already shows what they would get.
Fragments that take a parameter
A fragment that depends on a query parameter, such as a product id for a member price, still calls preventCaching(). If you ever make such a fragment cacheable, for example because its answer is the same for every visitor, make sure the parameter is part of the cache key. Reading it with $_request->getParameter('product') does that by itself; a parameter read any other way, through getQueryString() for example, has to be declared with $_response->addCacheVaryParameter('product'). Otherwise the page cache ignores it, and the first visitor's answer is served for every product.
Checking that it works
The fragment is never stored. Request it signed out and read the response headers: Cache-Control: no-store and X-Cache-Status: miss, no-store. Any other X-Cache-Status means the template is not switching the cache off.
The page has nothing personal in it. Sign in, load a cached page, and view the source rather than the inspector: the placeholder should hold the signed-out version, and no name, count or token.
It degrades. Turn JavaScript off: the page still works, showing the signed-out version.
A development environment stores no pages at all, so a page that works there can still show the wrong visitor's data on production. Check the headers on production, or on a staging environment that runs as production, before launch.