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 id | Route | Authoring | Notes |
|---|---|---|---|
home | / | main preferred | Legacy header / hero / main / footer sections still render when main is empty |
product | /p/:id and /products/:id | main only | Both URLs render the same PDP — no redirect |
checkout | /checkout | main only | Cart checkout; use cart_order_form in a form slot |
thank_you | /thanks | main only | Order context arrives via query params; legacy /thank-you redirects |
products | /products | main preferred | Collection page — see the filterator |
contact | /contact | main preferred | Legacy 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.footerProduct, 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.footerInside 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_formalso 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 —
| Param | Meaning |
|---|---|
order / id | Order id |
name / customerName | Customer name |
total / amount | Display total |
phone | Customer 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:
| Prop | Type | Purpose |
|---|---|---|
title, subtitle, description | string / text | Headline copy |
showConfetti | boolean | Celebration animation |
actions | array of { url, label, style } | Buttons (default / outline / secondary) |
features | array 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/:idand/products/:idshare the same loader — both fully render, neither redirects. Product links in theme code conventionally use/products/followed by the productslug(falling back toid). - 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_buttonor an equivalent theme float scrolling to#feeef-order-buy.
Authoring checklist
pages/<page>/components/*.tsx stacks; add a shell folder only when you need slots.shared/components/ and is placed with $ref.cart_order_form; the PDP gets product_order_form (or a slot-composed custom form).npm run build, then preview the full flow — home → product → checkout → thanks — with npm run dev (draft preview).Pitfalls
| Bad | Good |
|---|---|
New theme with header / hero / footer sections | Everything under the single main section |
| Deep nested shells on marketing pages | Flat sibling stacks |
"Check out" linking to /cart | Link to /checkout |
Redirecting to /thank-you | /thanks (the legacy URL only survives as a redirect) |
| Thanks page that invents its own state | Read order / name / total / phone from the URL |
| PDP price tag ignoring the selected offer | Strike the catalog price, show the offer unit |