ApplicationsStorefront

Pages and routes

Complete route map for the storefront application.

The storefront uses Next.js App Router with server components for catalog pages and client components for interactive cart/search UI.

Every store is served from one route tree under app/[host]/. proxy.ts rewrites /{path} to /{host}{path}, so each store's pages are built and cached separately, keyed by its host. Pages read the host with host() from next/root-params; Route Handlers and Server Actions, which have no root params, read the Host header with requestHost() from lib/store-host.ts.

Page routes

RouteTypeDescription
/ServerHome — hero banner, featured products, category links
/productsServerProduct listing with search query and sort params
/products/[slug]ServerProduct detail — gallery, variant selector, add-to-cart
/categories/[slug]ServerCategory-filtered product grid
/cartServer + ClientFull cart page with summary and checkout CTA
/checkoutServer + ClientCheckout form — customer info, shipping address, place order
/order-confirmationServerPost-payment order summary (query param: order ID)
/loginClientCustomer login form
/registerClientCustomer registration form
/accountServerAccount overview — profile info, recent orders
/account/ordersServerFull order history (requires auth)

Catalog pages have no loading.tsx. Each page wraps its content in <Suspense> with a skeleton, so the store's header and footer form a static shell that's shown at once, even for a product that hasn't been built yet.

For search engines and agents

RoutePurpose
/robots.txtAllows the catalog, disallows cart, checkout and account, points at the sitemap
/sitemap.xmlSitemap index: pages.xml, then one products-N.xml per 500 products
/sitemap/[file]One sitemap file, with product images
/llms.txtWhat the store is and where its Markdown pages are (llmstxt.org)
/index.md, /products.md, /products/[slug].md, /categories/[slug].mdMarkdown versions of the catalog pages

A catalog page is also served as Markdown when the request's first Accept type is text/markdown. HTML pages link to their Markdown version with <link rel="alternate" type="text/markdown"> and carry schema.org JSON-LD: Product and BreadcrumbList on product pages, CollectionPage on categories, OnlineStore and WebSite (with a SearchAction) on the home page.

Layout structure

Drag to pan · Scroll to zoom

API routes (BFF)

Local BFF routes proxy to the Commerce API where browser UX requires them:

RouteMethodsPurpose
/api/auth/[...all]*Better Auth handler
/api/commerce/cartGET, POSTRead existing cart or create new cart
/api/commerce/cart/itemsPOSTAdd item to cart
/api/commerce/cart/items/[itemId]PATCH, DELETEUpdate quantity or remove item
/api/commerce/searchGETProduct search for client search palette
/api/commerce/countriesGETCountry list for address forms
/api/revalidatePOSTExpires a store's cached catalog (called by the API and dashboard)

Why BFF routes exist

The cart ID is stored in an HTTP-only cookie (commerce_cart_id). Client-side cart operations (add to cart from product page, update quantity in drawer) cannot read this cookie directly — the BFF reads it and forwards the request to the API with the correct cart ID and tenant context.

Catalog reads do not need BFF routes — they are fetched server-side in React Server Components.

Components

Layout

ComponentFilePurpose
Headercomponents/header.tsxLogo, navigation, search, cart button, auth links
Footercomponents/footer.tsxStore links, legal, social
Searchcomponents/search.tsxSearch palette trigger
CartButtoncomponents/cart-button.tsxCart icon with item count badge
MobileMenucomponents/mobile-menu.tsxResponsive navigation drawer
ThemeTogglecomponents/theme-toggle.tsxLight/dark mode switch

Commerce

ComponentPurpose
ConnectedProductGridServer-fetched product grid with add-to-cart
AddToCartVariant-aware add-to-cart button
CheckoutFlowMulti-step checkout form

Providers

ProviderPurpose
AppProvidersTheme, toast (sonner), cart context wrapper
CartProviderClient cart state — syncs with BFF routes

Data fetching

Catalog reads live in lib/catalog.ts. Each takes the store host explicitly and is a "use cache" function; lib/commerce-data.ts wraps them with host() for pages:

ts
import { fetchProductList, fetchCategories } from '@/lib/commerce-data'

const [{ products }, categories] = await Promise.all([fetchProductList({ sort, page }), fetchCategories()])

Catalog requests send only the store host (x-storefront-host), never cookies, so one cached answer serves every shopper. Cart, order and account reads use getCommerceApi(), which forwards the session cookie and is never cached.

Caching

DataCache tagExpired by
Products (lists, product pages)products-{tenantId}Product, variant, inventory and category changes
Categoriescategories-{tenantId}Category changes
Store detailsstore-{tenantId}Store settings and payment integration changes

Tags come from storeCacheTag() in @prood/types. When the API or dashboard changes a store, @prood/commerce expires the tags locally and then, after the response, POSTs them to the storefront's /api/revalidate with STOREFRONT_REVALIDATE_SECRET. The next visit rebuilds the page. If that call fails, entries still expire within an hour.

Free-text search isn't cached: there's no end to the queries. Cart, checkout and account pages are never cached: they depend on cookies and session state.

Not found

Unknown products, categories and policy pages get a real 404 status. proxy.ts checks document requests and rewrites misses to the store's not-found page. Paths that match no route at all use app/global-not-found.tsx (experimental.globalNotFound), because the root layout sits under [host]. Hosts with no store redirect to the marketing site.

Building

With Cache Components, generateStaticParams in app/[host]/layout.tsx must name at least one host. It names build.invalid, a host that is no store's and renders as not found, so next build needs neither the database nor the API. Real stores are built on their first visit.