Guide 07 · Developer docs

Members, accounts and gated content

Building a member area on Core dna: who a member is in the data, the sign-in flows and their templates, single sign-on and passkeys, the my-account routing convention, members-only content, sessions and the page cache, and the security checks to run before launch.

L2
Unit 06 · Sessions, cookies and the cache

Member pages and the page cache

The page cache serves a stored copy to anyone who asks, signed in or not, and never stores what a signed-in visitor sees. What that does to a member site, how to measure it, and the cached shell with member fragments that keeps public pages fast.

Who this is for. The theme developer planning a member site, before building it. Afterwards you know which pages a member is served from the cache, which are rendered for them, and how to keep the public pages cached without showing members the wrong thing.

The rule, in two halves

  • A stored copy is served to everyone. The page cache is read before the platform looks at any cookie or header, so a URL with a stored copy answers with that copy, whoever asks. A signed-in member is served the copy rendered for an anonymous visitor.
  • Nothing rendered for a signed-in visitor is stored. A member's own render is never written to the cache, so a member never sees another member's page.

A stored copy is keyed on the hostname, the path and any query parameters the page declares; no cookie is part of the key. Authenticated requests and the cache covers the mechanism and the other cache layers.

What it does to a member site

  • Member pages render every time. A page under /my-account is never stored: signed-out visitors are redirected, and a member's render is not written. Its cost is its render, so keep member templates light (The three query-per-row shapes).
  • Personal output on a public page is wrong for members. Once anyone has visited the page signed out, members get that copy: a header that says "Sign in", no personal panel, and no CSRF token, so a member form on that page fails with a 403. Testing signed in on a cold cache never shows it.
  • Keep preventCaching() scoped to what needs it. On a whole public page it makes every visit a full render, for everyone; called from a base template, it switches the page cache off for the whole site. Call it on the member-specific part alone, so the rest of the site keeps the benefit of caching as traffic grows.
  • Reserve enforceCaching() for pages that look the same to every visitor. It stores the page even when a member rendered it, and the stored copy is then served to everyone who asks for that URL, signed out included. Never call it on a page that shows anything about the visitor.

Measuring it

Two response headers tell you which case a response is. X-Cache-Status says whether it came from the page cache (hit) or was rendered (miss, store when the render was stored, miss, no-store when it was not). X-Authenticated is 1 when the response was rendered for a signed-in user, and 0 otherwise, including on every stored copy. Development environments never store pages, so measure on production or a staging environment that caches:

Shell
curl -s -o /dev/null -D - https://www.example.com/events | grep -iE 'x-cache-status|x-authenticated'
x-cache-status: hit
x-authenticated: 0

Then open the same URL signed in as a member and read the headers in the browser's network panel. hit with X-Authenticated: 0 on a page that should know the member means the member was served the anonymous copy. miss, no-store on every request to a public page means caching is off for it, and every visitor pays for a full render. Measuring cache effectiveness covers sites behind the platform's CDN, where a miss is labelled differently.

A cached shell with member fragments

The answer is to cache the page for everyone and load the member-specific parts separately. The shell renders the signed-out version of each personal part, so it is correct for anyone and safe to store. A small fragment, rendered per visitor and never stored, supplies the member's version, and a script swaps it in after the page has painted. For a site header:

HTML In the cached shell
<div id="account-links" data-fragment="/fragments/account">
  <a href="/login">Sign in</a>
</div>
Template code templates/fragments/account.html
{$_response->preventCaching()}
{if $_currentUser->isLoggedIn()}
  <a href="{$_url->getPath('MyAccount', 'account')}">{$_currentUser->getFullName()|escape}</a>
  <a href="{$_url->getPath('logout')}">Sign out</a>
{else}
  <a href="{$_url->getPath('login')}">Sign in</a>
{/if}
{% do _response.preventCaching() %}
{% if _currentUser.isLoggedIn() %}
  <a href="{{ _url.getPath('MyAccount', 'account') }}">{{ _currentUser.getFullName()|e }}</a>
  <a href="{{ _url.getPath('logout') }}">Sign out</a>
{% else %}
  <a href="{{ _url.getPath('login') }}">Sign in</a>
{% endif %}
JavaScript In the site's script bundle
const slot = document.getElementById('account-links');
if (slot) {
  fetch(slot.dataset.fragment, { credentials: 'same-origin' })
    .then((response) => (response.ok ? response.text() : null))
    .then((html) => { if (html) slot.innerHTML = html; });
}

The fragment needs a URL of its own. Hybrid builds, in its section on dynamic fragments, sets one up with a Fragment layout and a page per fragment, and shows how the same fragment hands over the CSRF token that a cached page cannot carry. The recipe is the how-to Show logged-in state on a cached page.

Keep fragments small: each one is a render on every page view, for every visitor. The parts that belong in one are few: the member's name and account links, the cart count, the CSRF token, and a personal panel such as "your courses". Everything else stays in the shell, cached.

Go deeper

Was this page helpful?

Your answer goes to the docs team.