A Core dna store is a set of records and an API that moves a purchase from cart to order: products described in every language and priced in every currency the store sells in, a cart, a checkout that becomes an order, invoices, payments, tax and shipping. The site builds the storefront on top: the cart and checkout pages, their scripts, and the customer emails.
L2–L3
Unit 02 · Carts, cookies and the cache
The cart count on a cached page
Carts, cookies and the cachePage 7 of 31Updated
A page the page cache stores cannot carry one visitor's cart. The rule, the two things that go wrong when a header prints the cart, and the pattern that fixes it: a placeholder in the template and a count filled in by the browser from the cart API.
Who this is for. The theme developer building a site header with a cart icon, and whoever is asked why "the cart count is wrong". Afterwards you can say exactly where the cart may be read in a template, and you have the script that shows a correct count on every page.
The rule
Never print the cart into a page that the page cache can store. That means no cart count, mini-cart or total from $_cart in a header, footer or any template that renders a public page. Render a placeholder, and fill it in the browser.
The symptom this prevents is the single most common cart complaint: the count is wrong, stale, or someone else's. The cart count is wrong or stale starts from the symptom and arrives here.
What goes wrong
Two things, and both are invisible while developing, because development environments do not cache pages:
One visitor's cart is shown to everyone. The page cache stores the page as it was rendered for the visitor who first asked for it, and serves that copy to every later visitor, whatever cookies they send, until it expires or the content changes. A header that printed "3 items" for that visitor says "3 items" to the next thousand. How the cache decides what to store is on preventCaching() and fragments.
Every visitor gets a cart record. Reading $_cart saves an empty cart for a visitor who has none, and sets the cart cookie, so a header that reads it creates a database row for every crawler and every first-time visitor whose request renders the page.
$_cart is safe where the page is never stored for others: the cart page itself (/cart, which the platform never caches), the checkout pages, and member pages under /my-account, which are only ever rendered for a signed-in member.
The pattern
The template prints the same markup for everyone, with an empty count:
The site's script asks the cart API for the visitor's cart once the page has loaded, counts the available lines, and fills the badge. A visitor with no saved cart gets a 404, which here means an empty cart, and asking does not create one. Every cart change answers with the whole cart, so the add-to-cart code updates the badge from that answer without asking again:
JavaScriptsrc/js/cart-count.js
const badge = document.querySelector('[data-cart-count]');
export function countItems(cart) {
return (cart.items || [])
.filter((item) => item.available)
.reduce((count, item) => count + item.quantity, 0);
}
export function showCount(count) {
if (!badge) return;
badge.textContent = count;
badge.hidden = count === 0;
}
export async function refreshCartCount() {
const selector = encodeURIComponent('{items{quantity,available}}');
const response = await fetch(`/cdna-api/cart?properties=${selector}`, {
headers: { Accept: 'application/json' },
credentials: 'same-origin'
});
if (response.status === 404) { showCount(0); return; } // no cart yet
if (!response.ok) return; // leave the badge as it is
const { data } = await response.json();
showCount(countItems(data));
}
refreshCartCount();
// After any cart change, use the cart in the answer instead of asking again:
// const { data } = await response.json();
// showCount(countItems(data.cart));
Three details matter:
Read the cart through the API, not the cookie. The cart cookie is HttpOnly, so scripts cannot see it, and its value is only an id in any case.
Count available lines only. An unavailable line stays in the cart but is not charged, so a count that includes it disagrees with the cart page.
The request is cheap and uncached. API responses never come from the page cache, so the count is always the visitor's own.
The same approach serves everything else that depends on the visitor on an otherwise public page: the wishlist heart on a product card, a "signed in as" greeting, a member price. Hybrid builds describes the pattern in general, and Show logged-in state on a cached page applies it to the sign-in state.
Test it with the cache on
Development environments do not store pages, so a header that prints $_cart looks right there. Before launch, load a product page in two different browsers on staging, add an item in one, and reload the other: its count must stay at zero.