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.
A cart count in a cached header is wrong for almost everyone. Leave it empty in the template, read the visitor's cart from /cdna-api/cart after load (a 404 means no cart), and re-render from the cart your own add-to-cart calls return.
Context
Context. A header cart count rendered by the template is right for whoever caused the page to be cached and wrong for everyone after them. Read it in the browser instead. The cart API needs no fragment: GET /cdna-api/cart returns the current visitor's cart from their session, is never cached, and works the same from the browser whether or not the visitor is signed in. When the visitor has no cart yet it answers 404 rather than creating one, so calling it on every page view has no side effects. Update the count again whenever your own scripts change the cart.
Steps
Render the badge in the template with no number, hidden, so the cached copy shows nothing wrong.
After load, request the cart with properties asking only for item quantities.
Treat a 404 as an empty cart. Sum the quantities and show the badge only when the sum is above zero.
Listen for a cart:changed event that your add-to-cart code dispatches with the cart it got back, and re-render from that without another request.
Code
Template code
{* header partial: no number in the cached copy *}
<a href="/cart" class="cart-link">Cart <span class="cart-count" data-cart-count hidden></span></a>
{# header partial: no number in the cached copy #}
<a href="/cart" class="cart-link">Cart <span class="cart-count" data-cart-count hidden></span></a>
JavaScript
// cart-count.js
const badge = document.querySelector('[data-cart-count]');
function show(cart) {
const count = (cart?.items ?? []).reduce((sum, item) => sum + item.quantity, 0);
badge.textContent = count;
badge.hidden = count === 0;
}
async function refresh() {
const url = '/cdna-api/cart?properties=' + encodeURIComponent('{ cart { items { quantity } } }');
const response = await fetch(url, { credentials: 'same-origin', headers: { Accept: 'application/json' } });
if (response.status === 404) return show(null); // no cart yet
if (!response.ok) return; // leave the badge as it is
const { data } = await response.json();
show(data.cart);
}
document.addEventListener('cart:changed', (event) => show(event.detail));
refresh();
// in your add-to-cart code, after a successful request:
// document.dispatchEvent(new CustomEvent('cart:changed', { detail: data.cart }));
Why this and not that
Not preventCaching() on every page to render the count server-side: the header is on every page, so that turns the whole site's cache off for one number. Not a count kept in localStorage: it drifts from the real cart the moment a session expires, a discount removes a line or the visitor shops in two tabs. Not polling: the cart only changes when your own code changes it, and that code already has the new cart in its response. If the page already fetches an account fragment, the count can be rendered into that fragment instead, saving a request; the logged-in state how-to shows that fragment.