Templates read data from two sources: the context globals (the
StorefrontContext, see Globals) and the
extra.* bag, which the route handler stuffs with page-specific data.
This guide is the practical map of what’s available on each page type,
following the default theme’s usage.
Context globals by page type
| Page key | Always available | Present on this page |
|---|---|---|
index |
shop, settings, collections, linklists, request, extra |
, (sections carry their own data via extra) |
product |
same | product (full: variants, metafields, tags), cart (if a cart token is present) |
collection |
same | collection; listing data in extra.listing |
catalog |
same | listing data in extra.listing |
search |
same | search.query / search.results_count; results in extra.listing |
cart |
same | cart (server-rendered page) |
checkout |
same | order_id (confirmation branch) |
page (CMS) |
same | page (handle, title, content_html) |
blog / article |
same | blog, article |
account/* |
same | session-driven; client-rendered via data-* |
Missing globals render empty, never crash (product.name on an index page
→ "").
The extra.* contract
extra is a catch-all map the route handler fills per request. The
default theme depends on these keys, if you build a theme, negotiate this
contract with the API:
| Key | Shape | Consumed by |
|---|---|---|
extra.listing |
{ items, total, filters?, … } |
catalog_products, collection_products, search_results, collection_filters |
extra.listing.filters |
[{ groups: [{ options: [{value,label,active,count}] }] }] |
collection_filters |
extra.sort |
string | listing_toolbar |
extra.active_collection |
{ slug } |
catalog_products |
extra.collection |
{ name, description, slug } |
collection_products |
extra.collections |
[{ slug, name, image_url }] |
collections_list |
extra.featured_products |
product array | featured_products, product_recommendations (fallback) |
extra.recommendations |
product array | product_recommendations |
extra.query |
string | search_results (search bar value) |
extra.order_id / extra.order |
, | checkout, account/order |
extra.register_message |
string | account/register |
extra.reset_token |
string | account/reset_password |
Products in extra arrays are plain dicts with slug, name,
featured_image / image_src, base_price / price_range_min /
price, compare_at_price, currency_code, on_sale, consumed by the
product_card macro. They are not ProductObjects (no .variants,
no .metafields), because they come from the listing queries, not the
product detail query.
The product object on PDP
On product pages, product is a full ProductObject:
- Scalars:
name,slug,description,base_price,compare_at_price,currency_code,available,featured_image,tags,category_id. product.variants, array of{ id, title, price, available, metafields };variant_pickerandbuy_buttonsbranch onproduct.variants|length > 1.product.metafields, the lazy map:product.metafields.custom.rating(see Data layer).
The default theme’s PDP (product_detail.jinja) is the reference consumer:
it renders gallery + info columns by dispatching over children, and
references product.* for prices (| money), compare-at + discount
chips, vendor, share links (request.path | urlencode), and metafield
blocks.
Cart and forms
The theme wires cart behavior through main.js with data-* hooks, not
through bespoke endpoint knowledge:
| Hook | Purpose |
|---|---|
data-cart-badge |
cart count in navbar |
data-cart-toggle / data-cart-backdrop / data-cart-close |
drawer open/close |
data-add-to-cart + data-variants |
PDP add-to-cart (fetch, serialized) |
data-quick-add |
product-card quick add |
data-qty-stepper + data-action |
quantity steppers |
data-rr-cart-remove / data-rr-cart-item |
cart page remove |
data-rr-newsletter-form / data-rr-contact-form |
fake-submit forms |
data-rr-auth-form |
account auth forms |
data-rr-checkout-form + data-rr-checkout-* |
checkout form + client summary |
The cart is mirrored client-side in localStorage (ringroad:cart) and
synced with the server-rendered cart page. buy_buttons posts directly to
/api/v1/shop/{shop.domain}/cart/items server-side, the one endpoint the
default theme knows about.
Request metadata
request.path and request.page_type enable conditional rendering
(share links, active nav states). shop.domain builds store-scoped URLs.
The smoke test contract
tests/default_theme_smoke.rs renders the real default theme with stub
data, if you change what your templates read from globals or extra,
that test (or the fixtures feeding it) is where the renderer’s view of
your contract is pinned.