How-to library · Developer docs

How-to library

Task-shaped pages: a title that starts with a verb, the situation in a few lines, the steps, the working code in both engines, and why this way rather than another. Found from a search, done, and left.

Add a dynamic fragment to a page that is otherwise cached

A cached page is served to everyone, so per-visitor content goes in a fragment: a small uncached page, fetched after the cached one has painted, replacing a safe default. One layout, one page per fragment, and one line that turns caching off.

Context

Context. The platform caches whole pages and serves the cached copy to anonymous and signed-in visitors alike, so anything per visitor on a cached page is wrong for somebody. A fragment solves this: a small piece of the page rendered on its own, uncached, and fetched by the browser after the cached page has painted. The page keeps a sensible default in the fragment's place, so it still works while the fragment loads or if it fails. On Core dna a fragment is simply a page with a layout that renders one small template and turns caching off. This is the general recipe; the Hybrid page explains when you need one.

Steps

  1. Create one layout, "Fragment", whose template includes a fragment template named after the page's slug.
  2. In the admin, create a parent page /fragments and a child page per fragment (account, cart-count, …), each with the Fragment layout.
  3. Write the fragment template under templates/fragments/. Its first line switches caching off.
  4. In the cached page, put a placeholder holding the anonymous default.
  5. Fetch the fragment after load and replace the placeholder. Dispatch an event so other code can wait for it.

Code

The Fragment layout's template:

Template code
{* templates/pages/pages/fragment.html *}
{include "fragments/{$page->getSlug()}.html"}
{# templates/pages/pages/fragment.html #}
{{ include('fragments/' ~ page.slug ~ '.html') }}

A fragment. The first line is the part that makes it work:

Template code
{* templates/fragments/greeting.html *}
{$_response->preventCaching()}
{if $_currentUser->isLoggedIn()}
  <p>Welcome back, {$_currentUser->getPerson()->getFirstName()|escape}.</p>
{else}
  <p>Welcome.</p>
{/if}
{# templates/fragments/greeting.html #}
{{ _response.preventCaching() }}
{% if _currentUser.isLoggedIn() %}
  <p>Welcome back, {{ _currentUser.getPerson().getFirstName()|e }}.</p>
{% else %}
  <p>Welcome.</p>
{% endif %}

The placeholder and the fetch, in the cached page:

Template code Smarty only
<div data-fragment="/fragments/greeting"><p>Welcome.</p></div>

<script nonce="{$_inlineScriptNonce}">
  document.querySelectorAll('[data-fragment]').forEach(async (slot) => {
    try {
      const response = await fetch(slot.dataset.fragment, { credentials: 'same-origin' });
      if (!response.ok) return;              // keep the default
      slot.innerHTML = await response.text();
      document.dispatchEvent(new CustomEvent('fragment:ready', { detail: slot.dataset.fragment }));
    } catch (e) { /* offline: keep the default */ }
  });
</script>

On a Twig site the nonce is written {{ _inlineScriptNonce }}; the rest of the script is the same.

Why this and not that

Not preventCaching() on the whole page: it works, and it makes every visit a full render for the sake of one line. Not a JSON API call and client-side templating for markup: the fragment reuses your templates and escaping, and stays a server-rendered piece of HTML. Not a fragment that reads a query string without saying so: the page cache ignores a query string unless the template reads it with $_request->getParameter(), which declares it to the cache by itself, or declares it explicitly with $_response->addCacheVaryParameter(), so a fragment that depends on one must either call preventCaching(), as here, or read or declare the parameter. Otherwise the first visitor's answer is cached and served to everyone. Twig escapes nothing by default on this platform, hence |e on the name.

Go deeper

Was this page helpful?

Your answer goes to the docs team.