Customer storefront
The customer-facing routes that make up the shop and the rendering model behind them.
The customer storefront covers the routes a shopper actually sees. Every route lives under the language segment (app/[locale]/…), so with the default urls.localePrefix: "always" a product answers at /en/product/<slug> in a shop from npx create-cartwright, which starts English-only, and at /da/product/<slug> as well once Danish is in brand.locales (URL options drops the prefix on a one-language shop). The segments are English, except the catalogue at /produkter and /priser, a pricing page that renders only on a website-mode shop with industryTemplate: "saas". The Danish paths of older releases — /kategori, /produkt, /kurv, /ordre, /konto, /kontakt, /anmeld, with or without the language in front — are not routes: they answer with a permanent 301 to their English twin (LEGACY_SLUG_MAP in proxy.ts), so old bookmarks and search results still land.
Route map
/[locale]— Landing. Hero, featured categories, trust band, AI stylist FAB (ifbrand.config.ts:features.aiStylistis on)./[locale]/produkter— All products. Filterable list./[locale]/category/[slug]— Category-scoped product list with hero, description, FAQ block, and optional video./[locale]/product/[slug]— Product detail. Images, copy, price, variants, add-to-cart, related products./[locale]/cart— Cart. Line items with quantity steppers and a remove button, then subtotal, shipping and total. Shipping is the flat rate frombrand.config.tspolicies(free above its threshold), shown as one line; the cart takes no discount code (app/[locale]/cart/page.tsx)./[locale]/checkout— Delivery details (name, email, address, postcode, city) and an optional discount code in the checkout form (components/CheckoutForm.tsx), then Stripe's Payment Element, which the form renders throughcomponents/StripePaymentPanel.tsx. There is no delivery-method choice: every order pays the cart's flat shipping./[locale]/order/[id]— Order confirmation. Linkable for receipts./[locale]/account— Account hub. Sign in with a password (or a magic link once Resend is set up), view orders, keep a shipping address./[locale]/info/[slug]— CMS-style static pages rendered from the admin Sider CRUD./[locale]/info— An index of the information pages: about, contact, privacy, terms and cookies, then every other published page, withCollectionPageJSON-LD. From v0.63.0; before it the bare/infowas a 404. Not in a--profile sitescaffold, which has no CMS.
Nested categories
From v0.63.0, the category page and the product page follow a category's parent (Category.parentId). A shop whose categories are not nested renders exactly as before, and its product pages do not even read the category tree.
- A parent category lists the products of every category below it, not only the products placed in it directly. The page finds products by their primary category (
Product.categoryId), so a shop that keeps its products in the innermost categories used to show an empty page for every parent. The product count in the page's hero and itsItemListJSON-LD cover the same products. - The breadcrumb walks the parent chain: Home › All products › each parent, top level first › the category, with the product added at the end on a product page. On the shared trail, the visible breadcrumb and the
BreadcrumbListJSON-LD carry the same steps (the exceptions follow below). Each parent's name uses its stored translation for the page's language, as the category's own name does, and its link goes through the same helper as every other category link, so it follows a stored permalink onceurls.permalinksis on. As before, the visible trail shows only when thebreadcrumbsruntime flag is on (default off); the JSON-LD is always emitted. The halo and crema packs draw their own short product-page trail, which has no categories in it; theirBreadcrumbListstill gets the full chain. - Depth and loops. Going up, the walk has to reach a top-level category within 10 steps. A category set as its own parent, a loop (A under B under A) or a chain of more than 10 parents gets no parent steps at all: the page shows the flat trail, Home › All products › the category (› the product on a product page), so
BreadcrumbListnever claims two contradictory parents. Going down, each subcategory is included once, however the rows loop, and the walk stops 10 levels below the page's category. If reading the tree fails, the trail falls back to the flat one and the category page to its own products. - Setting a parent. Nothing in the engine sets one yet: the admin category form, the
categories.upserttool, the product CSV import and a sitepack export or restore carry no parent, so a tree exists only where something writesCategory.parentIdin the database directly. The case it was built for is a migrated WooCommerce catalogue, whose products sit in the innermost categories; the dedicated WooCommerce importer is planned, not built. - The filter, the agent search and the counts follow the same tree. The catalogue's category filter (
/produkter?kategori=<slug>) and thecategorySlugfilter of theproducts.searchtool (the one AI agents use) match the products of the category and of every category below it — the list the category page shows; each product still names its own category.categories.listkeepsproductCountas the products placed in the category itself and addsproductCountWithDescendantson a category that has subcategories, and/admin/kategoriershows a parent's count as "direct (total)". On v0.62.0 the filter, the search and the counts read only products placed in a category directly, so filtering by a parent whose products all sit in subcategories returns no products there. - Extra categories. A product can be filed under extra categories beside its primary one (the
ProductCategorytable, as WooCommerce does). From v0.63.0 it is listed like a primary member in each of them and in every category above them — on the category page with its count andItemList, in/produkter?kategori=and inproducts.search— once each, andcategories.listand/admin/kategoriercount it the same way. Its URL, canonical, breadcrumb and the categoryproducts.searchreports for it stay its primary category's. Nothing in the engine writes those rows yet (no admin field, tool, CSV or sitepack column); a shop without them gets the same answers as before.
Rendering model
Cartwright uses Next.js App Router. Pages are React Server Components by default; interactive surfaces (cart, checkout, AI chat) are client components nested under server pages.
- Product, category, and content pages are SSR + cached. Cache invalidation triggers on admin writes via
revalidatePathin the corresponding server actions. - The cart is cookie-backed (server-readable) for unauthenticated browsers; merged into the customer record on sign-in.
- The AI stylist FAB is a client component that opens a
/api/assistant/chatstream; nothing on the storefront ships AI to the bundle if the feature flag is off.
Layout composition
app/layout.tsx sets the fonts, the theme and the cookie consent; app/[locale]/layout.tsx renders the header and footer — the shared ones, or the active design's own chrome — around every storefront page. There is no category layout: the category page writes its own Open Graph metadata and its BreadcrumbList, FAQPage and ItemList JSON-LD in app/[locale]/category/[slug]/page.tsx.
brand.config.ts:uiLabels provides every customer-facing string. A fork typically translates that block (it is currently Danish) before changing any component code.
Cart cookie + shipping
Anonymous shoppers get a cart_id cookie; the cart row in Prisma is keyed off it. When a shopper signs in via magic link, the unauthenticated cart merges into the customer's row. lib/shipping-cookie.ts stores the chosen shipping method between visits.
The cart is server-readable so it can survive page reloads and render hydration-free. The trade-off: every cart mutation is a server action, not a client-side optimistic update. That keeps Stripe-side totals trustworthy.
Performance posture
Category and product pages prerender at deploy time when possible. Cart and checkout are dynamic. Images go through next/image with images.remotePatterns covering Unsplash, the Vercel Blob domain, and any external CDN you wire into brand.config.ts:images.
Agent editability
Make the shop's structure editable by AI agents and machine config — homepage section layout, theme tokens, and the catalog — via MCP tools and JSON files.
Storefront UX toggles
Small storefront UX flags — the first-visit welcome guide and the promo announcement bar — both runtime, default-off, ecommerce-only.