Routing

apps/storefront/src/proxy.ts is the Next.js middleware entry. The flow is:

  1. Dispatch — paths starting with /admin go to admin() (see src/middleware/admin.ts), everything else goes to storefront() (src/middleware/storefront.ts).
  2. storefront() reads req.headers.host, normalizes it (strips ports, .localhost, Vercel preview suffixes), then calls Shop.findByDomain(hostname), which resolves the tenant through the Convex db/shops query seam.
  3. On a hit, the resolved domain is injected into the URL so the App Router serves the page from src/app/[domain]/[locale]/….
  4. On NotFoundError, the middleware rewrites to SERVICE_DOMAIN/status/unknown-shop/. Other commerce errors → /status/unknown-error/.
  5. Unknown hosts in dev/preview fall back to nordcom-demo-shop.com so contributors can boot without seeding their own tenant.

When adding routes, place them under src/app/[domain]/[locale]/…not at the root. The [domain]/api/… segment is reserved for tenant-scoped API endpoints.

Product page parallel slots

The PDP at src/app/[domain]/[locale]/products/[handle]/ is the only route that uses parallel route slots. layout.tsx is the shell — it resolves the tenant, locale, and product once, emits the analytics trigger, breadcrumbs, and JSON-LD, then composes four named slots into the page frame:

  • @galleryProductGallery (images, thumbhash placeholders).
  • children (page.tsx) — purchase block: title, price, variant picker, add-to-cart.
  • @descriptionProductDescription (Shopify descriptionHtml + CMS overlay).
  • @detailsProductDetails (spec tables, metafields).
  • @recommendationsRecommendedProducts (full-width, below the fold).

Each slot is wrapped in <Suspense> with a skeleton fallback so slots can stream independently. generateStaticParams lives in a sibling module (static-params.ts) rather than in the layout — keep new PDP-adjacent concerns in their own files instead of growing the shell.

Loading UI coverage

The following routes have a loading.tsx neighbor (Next.js renders it as the Suspense fallback during navigation):

  • [locale]/loading.tsx — locale shell
  • [locale]/search/loading.tsx
  • [locale]/cart/loading.tsx
  • [locale]/account/loading.tsx
  • [locale]/countries/loading.tsx
  • [locale]/blogs/[blog]/loading.tsx
  • [locale]/products/[handle]/loading.tsx
  • [locale]/collections/[handle]/loading.tsx

When adding a new top-level route under [locale]/, add a loading.tsx alongside it so the navigation transition has a skeleton rather than blank chrome.

On this page