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.

Show logged-in state on a cached page

A cached page cannot know who is looking. Render the signed-out links, fetch an uncached account fragment after load, swap it in, and take the CSRF token from it for your API calls.

Context

Context. The page cache is read before the platform knows who is asking, and it can serve a signed-in visitor the copy it cached for an anonymous one. So a header that shows "Sign in" or "My account" from the template is wrong for somebody on any cached page. Put the account links in a fragment: the cached page shows "Sign in", and a small uncached request replaces it for whoever is signed in. The same fragment is the place to hand over the CSRF token, which a cached page cannot carry. This is the fragment the Hybrid page describes; the general recipe is in dynamic fragments.

Steps

  1. Set up the Fragment layout and a /fragments/account page, as in the dynamic fragment how-to.
  2. Write templates/fragments/account.html: caching off, the links for each state, and the token in a <template> tag.
  3. In the cached header, render the signed-out links as the placeholder.
  4. Fetch the fragment, swap it in, keep the token for your API module, and dispatch account:ready.

Code

Template code
{* templates/fragments/account.html *}
{$_response->preventCaching()}
{if $_currentUser->isLoggedIn()}
  <a href="/my-account">My account</a>
  <a href="/logout">Sign out</a>
{else}
  <a href="/login">Sign in</a>
{/if}
<template data-csrf="{$_csrfToken|default:''}"></template>
{# templates/fragments/account.html #}
{{ _response.preventCaching() }}
{% if _currentUser.isLoggedIn() %}
  <a href="/my-account">My account</a>
  <a href="/logout">Sign out</a>
{% else %}
  <a href="/login">Sign in</a>
{% endif %}
<template data-csrf="{{ _csrfToken|default('') }}"></template>
Template code Smarty only
<div id="account-panel"><a href="/login">Sign in</a></div>

<script nonce="{$_inlineScriptNonce}">
  fetch('/fragments/account', { credentials: 'same-origin' })
    .then(r => r.ok ? r.text() : Promise.reject(r.status))
    .then(html => {
      const tmp = document.createElement('div');
      tmp.innerHTML = html;
      const token = tmp.querySelector('[data-csrf]')?.dataset.csrf;
      if (token) window.csrfToken = token;          // your API module reads this
      document.getElementById('account-panel').replaceWith(...tmp.childNodes);
      document.dispatchEvent(new CustomEvent('account:ready', { detail: { signedIn: !!token } }));
    })
    .catch(() => { /* keep "Sign in" */ });
</script>

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

Why this and not that

Not a cookie check in JavaScript: the session cookie is not readable by scripts, and a flag cookie of your own says someone signed in at some point, not that they still are. Not preventCaching() on every page with a header: that is every page. Not a guess from /cdna-api/users/account: it works for members, but it is one more JSON endpoint to template in the browser, and it cannot deliver the CSRF token. The token is empty in the fragment for a signed-out visitor, which is also how the script knows which state it is in.

Go deeper

Was this page helpful?

Your answer goes to the docs team.