All store-scoped routes live under app/[domain]/. Below is what each route owns, what data it fetches SSR, and what it passes to the BYOT bundle.Home — [domain]/page.tsx#
| Step | What |
|---|
| SSR fetch | Store + theme settings |
| page.type | "home" |
| page.data | none (themes fetch products/collections via hooks) |
| Cache | ISR revalidate: 60 |
PDP — [domain]/products/[slug]/page.tsx#
| Step | What |
|---|
| SSR fetch | Store + theme + product (fetchProductBySlug → /products/{slug}) |
| page.type | "product" |
| page.data | { product: Product } (includes options[] + variants[]) |
| Built-in fallback | BuiltInProductDetail with variant picker + qty + add-to-cart |
| Cache | ISR revalidate: 300, tag: product:{storeId}:{slug} |
PLP — [domain]/collections/[slug]/page.tsx#
| Step | What |
|---|
| SSR fetch | Store + theme + collection + products in collection |
| page.type | "collection" |
| page.data | { collection: Category, products: Product[], pagination: {...} } |
| Cache | ISR revalidate: 60, tag: collection:{storeId}:{slug} |
Cart — [domain]/cart/page.tsx#
| Step | What |
|---|
| SSR fetch | Store + theme (no cart pre-fetch — useCart() runs client-side) |
| page.type | "cart" |
| page.data | none |
| Built-in fallback | BuiltInCart — line items, qty controls, applied promotion, totals |
Checkout — multi-step (Phase 7)#
Each step is its own route with its own BYOT fork.page.type: "checkout_contact"
page.data: { cart, customer }
Customer might be null (guest checkout)
Shipping — [domain]/checkout/shipping/page.tsx#
page.type: "checkout_shipping"
page.data: { cart, address, rates, pickup_locations }
Calls /api/shipping/options for rates + /api/storefront/pickup-locations for pickup tab
Payment — [domain]/checkout/payment/page.tsx#
page.type: "checkout_payment"
page.data: { cart, address, methods, saved_cards }
Methods filtered to those the merchant has enabled (via /api/storefront/checkout-config)
Saved cards loaded only for token-charge-capable gateways (Paymob, Kashier)
Built-in form includes the Phase 8.3 gift-card input
Review — [domain]/checkout/review/page.tsx#
page.type: "checkout_review"
page.data: { cart, address, rate, method }
Submit → POST /api/checkout
Processing — [domain]/checkout/processing/page.tsx#
page.type: "checkout_processing"
Polls /api/customer/orders/{id} every 2s until payment_status="paid" or terminal failure
Used when the gateway redirects back here mid-payment
Thank-you — [domain]/checkout/[order_id]/thank-you/page.tsx#
page.type: "checkout_thank_you"
Terminal page — order_completed funnel event fires here for online payments
Account routes — [domain]/account/*#
| Route | page.type | page.data | Guarded |
|---|
/account | account | { customer, orderCount, addressCount } | redirect to /login if no customer |
/account/login | account_login | {} | no |
/account/register | account_register | {} | no |
/account/recover | account_recover | {} | no |
/account/reset | account_reset | { token } | no |
/account/profile | account_profile | { customer } | yes |
/account/orders | account_orders | { customer, orders, pagination } | yes |
/account/orders/[id] | account_order | { customer, order } | yes (404 if not customer's order) |
/account/addresses | account_addresses | { customer, addresses } | yes |
/account/gift-cards | account_gift_cards | { customer } | yes |
/account/wishlist | account_wishlist | { customer, items } | yes |
Auth happens server-side via the customer_access_token cookie. The proxy forwards it to NUMU-api's /storefront/me/* routes which return 401 on miss → storefront redirect("/account/login").Search — [domain]/search/page.tsx#
| Step | What |
|---|
| SSR fetch | Store + theme + search results from /storefront/store/{id}/search?q= (Phase 4.1 tsvector) |
| page.type | "search" |
| page.data | { query, products, collections, pagination } |
Content routes#
Pages — [domain]/pages/[handle]/page.tsx#
page.data: { page: { title, body_html, handle } }
Resolves from store.pages[handle] — merchant-authored static pages
Policies — [domain]/policies/[handle]/page.tsx#
page.data: { policy: { handle, title, body_html } }
handle ∈ { privacy, refund, terms, shipping }
Stub-policies merchant can edit in hub Settings → Legal
Blogs — [domain]/blogs/page.tsx, [domain]/blogs/[handle]/page.tsx, [domain]/blogs/[blog]/[article]/page.tsx#
page.type: "blogs" | "blog" | "article"
Blogs backend lands in Phase 8.9. Current state: stubs that render gracefully when the backend returns empty.
Special#
Password gate — [domain]/password/page.tsx#
The middleware redirects all routes to /password when store.password_protected.enabled is true and the visitor doesn't have the unlock cookie.
404 — [domain]/not-found.tsx#
BYOT-forks like the others. Themes that ship a not_found section template render their custom 404. Built-in renders a generic "Page not found".
Error — [domain]/error.tsx#
Loads BEFORE the SDK federation runtime. Renders a static HTML template specified in theme.json's error_template_url (Phase 7.3). Falls back to a generic platform error page when missing.
Loading — [domain]/loading.tsx#
Same shape as error. Uses loading_template_url from theme.json.
SEO#
[domain]/sitemap.xml/route.ts — enumerates products/collections/pages dynamically
[domain]/robots.txt/route.ts — respects store.settings.seo.robots
Per-route generateMetadata() populates <title>, <meta>, OG tags
Locale URL prefix — Phase 6#
The middleware honors /ar/... prefixes:/ar/products/blue-shirt → strips /ar, sets numu_locale=ar cookie, rewrites to /products/blue-shirt
The route handlers don't see the /ar prefix — they always render the rewritten path. The cookie drives useLocalization() + useDirection() (RTL when ar).Adding a new route#
1.
Create app/[domain]/<your-path>/page.tsx
3.
Resolve via byot-fork.tsx:const fork = await resolveByotFork(domain, {
type: "my_route_type",
data: { ...whateverTheBundleNeeds },
});
if (fork.kind === "missing-store") notFound();
if (fork.kind === "byot") return fork.element;
return <MyBuiltInFallback />;
5.
Update the theme contract docs so theme devs know to handle it (or fall back to built-in)
Modified at 2026-09-24 13:03:12