.dev
Storefront templates

Standard pages

Checkout, thanks, and product pages — the single-main-section contract, slots, and the data each page receives.

Beyond the home page, three routes carry the purchase flow: the product page (PDP), /checkout, and /thanks. All of them follow the same authoring contract: one main section holding either a flat stack of components or a single shell whose layout lives in slots.

Page matrix

Page idRouteAuthoringNotes
home/main preferredLegacy header / hero / main / footer sections still render when main is empty
product/p/:id and /products/:idmain onlyBoth URLs render the same PDP — no redirect
checkout/checkoutmain onlyCart checkout; use cart_order_form in a form slot
thank_you/thanksmain onlyOrder context arrives via query params; legacy /thank-you redirects
products/productsmain preferredCollection page — see the filterator
contact/contactmain preferredLegacy named sections when main is empty

The renderer prefers main on every page and only falls back to the legacy multi-section tree (header / top / start / end / bottom / footer) when main is empty — so old store blobs keep working, but new themes never author multi-section pages.

Flat stack or shell + slots

Marketing pages are usually just a stack of siblings (which builds into sections.main.components):

pages/home/components/
  header.json        # $ref shared.header
  hero.tsx
  rail.tsx
  footer.json        # $ref shared.footer

Product, checkout and thanks typically want a shell: one custom component that owns the page frame while the merchant fills and reorders named slots:

pages/product/components/product-shell/
  product-shell.tsx      # export const meta + slotsSchema
  slots/header/...       # $ref shared.header
  slots/body/gallery.tsx
  slots/form/order.json  # { "type": "product_order_form", ... }
  slots/footer/...       # $ref shared.footer

Inside the shell's JSX, slots arrive pre-rendered:

function App() {
  return (
    <div>
      {props.slots?.header}
      <main>{props.slots?.body}</main>
      {props.slots?.form}
      {props.slots?.footer}
    </div>
  );
}

Only reach for a shell when you genuinely need slots — do not hand-build deep nested trees when a flat sibling stack does the job. The rules for slotsSchema and slot delivery are in the data model.

/checkout

The cart checkout page. Its required node is an order form for the whole cart:

  • Put type: "cart_order_form" in the shell's form slot. It handles multiple cart items; product_order_form also works when the cart's current item is enough.
  • The page receives the live cart through the normal scope (useFeeefCart()), not through props — an empty cart should render an empty state linking to /products.
  • On success the native cart form redirects to /thanks?order=&name=&phone=&total= with the created order's values filled in.
  • Cart drawers and mini-carts must link their "Check out" action to /checkout.

Everything about submission — drafts, validation, security — is the order form contract; the checkout page just hosts the cart-level variant.

/thanks

Rendered from the template page key thank_you. The page has no server data of its own: order context arrives as query params, all optional —

ParamMeaning
order / idOrder id
name / customerNameCustomer name
total / amountDisplay total
phoneCustomer phone

After a successful COD submit, redirect the shopper for example to /thanks?order={id}&name={encodeURIComponent(name)} (inside your form code). Never redirect for fake FuHe3nf order ids.

The built-in registry component thank_you reads those params and renders a confirmation with configurable props:

PropTypePurpose
title, subtitle, descriptionstring / textHeadline copy
showConfettibooleanCelebration animation
actionsarray of { url, label, style }Buttons (default / outline / secondary)
featuresarray of { emoji, title, description }Reassurance items

A custom thanks body works the same way — read the params with useSearchParams() and translate the copy with t().

Product page (PDP)

  • Two working routes: /p/:id and /products/:id share the same loader — both fully render, neither redirects. Product links in theme code conventionally use /products/ followed by the product slug (falling back to id).
  • The route loads the product by slug, sets it as the cart's current item and bootstraps shipping before your components render; custom JSX then reads it via useCurrentProduct():
function App() {
  const { product } = useCurrentProduct();
  const cartCtx = useFeeefCart();
  if (!product || !cartCtx) return null;
  // product.media is string[] of URLs; product.photoUrl is the cover
  return <h1>{product.name}</h1>;
}
  • Typical shell slots: header | body (gallery, price, description) | form (the order form) | footer.
  • Offer pricing rule: when the shopper selects an offer whose unit price differs from the catalog unit price, the PDP price tag must mute and strike through the catalog amount and show the offer price as the live one — re-subscribe via useFeeefCart() so it updates instantly.
  • The floating order CTA is part of the PDP contract — registry order_floating_button or an equivalent theme float scrolling to #feeef-order-buy.

Authoring checklist

Prefer flat pages/<page>/components/*.tsx stacks; add a shell folder only when you need slots.
Chrome used on 2+ pages goes to shared/components/ and is placed with $ref.
Checkout gets cart_order_form; the PDP gets product_order_form (or a slot-composed custom form).
Thanks page reads its query params; success redirects carry them.
Build with npm run build, then preview the full flow — home → product → checkout → thanks — with npm run dev (draft preview).

Pitfalls

BadGood
New theme with header / hero / footer sectionsEverything under the single main section
Deep nested shells on marketing pagesFlat sibling stacks
"Check out" linking to /cartLink to /checkout
Redirecting to /thank-you/thanks (the legacy URL only survives as a redirect)
Thanks page that invents its own stateRead order / name / total / phone from the URL
PDP price tag ignoring the selected offerStrike the catalog price, show the offer unit

On this page