Files
route-commerce/docs/NESTED_LAYOUTS.md
T
Tyler 9458fd0506
Deploy to route.crispygoat.com / deploy (push) Successful in 4m20s
docs: add nested layout skeleton map
Visualizes the current layout nesting in the App Router, identifies
gaps where shared sub-layouts could be added (cart/checkout, wholesale,
water, admin/communications, admin/settings), and shows how to add
a new nested layout or parallel route for modal-style navigation.
2026-06-16 23:29:16 -06:00

13 KiB

Nested Layout Skeleton

Visual map of how pages nest inside each other in the Next.js App Router. Each level persists across navigation — children swap, parents stay.

Mental model

┌─────────────────────────────────────────────────────────────────┐
│ app/layout.tsx              (root)                              │
│   html / body / fonts / Providers / Toast / CookieBanner        │
│   ─────────────────────────────────────────────────────────     │
│   No global header. Each route group renders its own chrome.    │
└─────────────────────────────────────────────────────────────────┘
                                  │
        ┌─────────────────────────┼─────────────────────────────┐
        │                         │                             │
        ▼                         ▼                             ▼
   Public routes            Storefronts                   Admin
   (/, /pricing,            (/tuxedo/*,                   (/admin/*)
    /contact, /login)        /indian-river-direct/*)

Full tree

src/app/
├── layout.tsx ────────────────────────────────────────────────── [ROOT]
│   • <html>, <body>
│   • next/font: Fraunces (display), Manrope (sans), Fragment_Mono
│   • <Providers> (theme, posthog, sentry, etc.)
│   • <ToastNotificationContainer />
│   • <CookieConsentBanner />
│   ─── pages render INSIDE ───
│   │
│   ├── error.tsx              [ROOT-LEVEL] Atelier error page
│   ├── loading.tsx            [ROOT-LEVEL] "Preparing your harvest"
│   ├── not-found.tsx          [ROOT-LEVEL] "Off the beaten path"
│   │
│   ├── page.tsx               /                  LandingPageClient
│   │   └── renders its own SiteHeader + Hero + SiteFooter
│   │
│   ├── login/
│   │   ├── page.tsx           /login             LoginClient (standalone)
│   │   └── loading.tsx        [isolated]
│   │
│   ├── cart/
│   │   ├── page.tsx           /cart              CartClient (standalone)
│   │   └── loading.tsx
│   │
│   ├── checkout/
│   │   ├── page.tsx           /checkout          CheckoutClient (standalone)
│   │   ├── loading.tsx
│   │   └── success/page.tsx   /checkout/success
│   │
│   ├── change-password/page.tsx
│   ├── pricing/page.tsx       + loading.tsx
│   ├── contact/page.tsx       + loading.tsx
│   ├── blog/page.tsx
│   ├── changelog/page.tsx
│   ├── roadmap/page.tsx
│   ├── waitlist/page.tsx
│   ├── security/page.tsx
│   ├── privacy-policy/page.tsx
│   ├── terms-and-conditions/page.tsx
│   ├── maintenance/page.tsx
│   │
│   ├── ─── STOREFRONTS ─────────────────────────────────────────
│   │
│   ├── tuxedo/
│   │   ├── layout.tsx ─────────────────────── [TUXEDO CHROME]
│   │   │   • Dark emerald gradient backdrop
│   │   │   • Ambient emerald orb (top-right blur)
│   │   │   • <StorefrontHeader> + <StorefrontFooter> per page
│   │   │   • BreadcrumbList + LocalBusiness JSON-LD in metadata
│   │   │   ─── INSIDE ───
│   │   │   ├── error.tsx
│   │   │   ├── loading.tsx
│   │   │   ├── page.tsx                   /tuxedo
│   │   │   ├── stops/
│   │   │   │   ├── page.tsx               /tuxedo/stops
│   │   │   │   ├── loading.tsx
│   │   │   │   └── [slug]/
│   │   │   │       ├── page.tsx           /tuxedo/stops/:slug
│   │   │   │       ├── error.tsx
│   │   │   │       └── loading.tsx
│   │   │   ├── about/
│   │   │   │   ├── layout.tsx
│   │   │   │   ├── page.tsx               /tuxedo/about
│   │   │   │   ├── error.tsx
│   │   │   │   └── loading.tsx
│   │   │   ├── faq/
│   │   │   │   ├── layout.tsx
│   │   │   │   ├── page.tsx               /tuxedo/faq
│   │   │   │   ├── error.tsx
│   │   │   │   └── loading.tsx
│   │   │   ├── contact/
│   │   │   │   ├── layout.tsx
│   │   │   │   ├── page.tsx               /tuxedo/contact
│   │   │   │   ├── error.tsx
│   │   │   │   └── loading.tsx
│   │   │   ├── products/
│   │   │   │   └── [slug]/page.tsx        /tuxedo/products/:slug
│   │   │   └── time-clock/page.tsx        /tuxedo/time-clock
│   │
│   ├── indian-river-direct/
│   │   ├── layout.tsx ─────────────────── [IRD CHROME]
│   │   │   • Light blue/white glass gradient backdrop
│   │   │   • Three decorative blue orbs
│   │   │   • <StorefrontHeader> + <StorefrontFooter> per page
│   │   │   • LocalBusiness (FL, family-owned 1985) JSON-LD
│   │   │   ─── INSIDE ───
│   │   │   ├── error.tsx
│   │   │   ├── loading.tsx
│   │   │   ├── page.tsx                   /indian-river-direct
│   │   │   ├── stops/
│   │   │   │   ├── page.tsx
│   │   │   │   └── [slug]/
│   │   │   │       ├── page.tsx
│   │   │   │       ├── error.tsx
│   │   │   │       └── loading.tsx
│   │   │   ├── about/   (layout + page + error + loading)
│   │   │   ├── faq/     (layout + page + error + loading)
│   │   │   └── contact/ (layout + page + error + loading)
│   │
│   ├── ─── ADMIN ──────────────────────────────────────────────
│   │
│   └── admin/
│       ├── layout.tsx ───────────────────── [ADMIN SHELL]
│       │   • dynamic = "force-dynamic"  (reads cookies)
│       │   • getAdminUser() → AdminSidebar + content area
│       │   • ToastProvider + ToastContainer
│       │   • Sidebar persists across all /admin/* routes
│       │   • Parchment background via .admin-section
│       │   ─── INSIDE ───
│       │   ├── error.tsx                  Admin error page
│       │   ├── loading.tsx                Skeleton: header + stats + table
│       │   ├── page.tsx                   /admin  (Dashboard)
│       │   │
│       │   ├── orders/
│       │   │   ├── page.tsx               /admin/orders
│       │   │   ├── [id]/page.tsx          /admin/orders/:id
│       │   │   └── new/page.tsx           /admin/orders/new
│       │   │
│       │   ├── products/
│       │   │   ├── page.tsx               /admin/products
│       │   │   ├── [id]/page.tsx
│       │   │   ├── new/page.tsx
│       │   │   └── import/page.tsx
│       │   │
│       │   ├── stops/
│       │   │   ├── page.tsx               /admin/stops
│       │   │   ├── [id]/page.tsx
│       │   │   └── new/page.tsx
│       │   │
│       │   ├── communications/
│       │   │   ├── loading.tsx
│       │   │   ├── page.tsx               /admin/communications
│       │   │   ├── compose/page.tsx
│       │   │   ├── campaigns/[id]/page.tsx
│       │   │   ├── contacts/page.tsx
│       │   │   ├── logs/page.tsx
│       │   │   ├── segments/page.tsx
│       │   │   ├── settings/page.tsx
│       │   │   ├── templates/page.tsx
│       │   │   ├── templates/[id]/page.tsx
│       │   │   ├── analytics/page.tsx
│       │   │   ├── abandoned-carts/page.tsx
│       │   │   └── welcome-sequence/page.tsx
│       │   │
│       │   ├── settings/  (sub-tree of 13 pages: billing, brand,
│       │   │                payments, shipping, ai, apps,
│       │   │                integrations, square-sync, ...)
│       │   │
│       │   ├── route-trace/  (lots, lookup, settings)
│       │   ├── me/
│       │   ├── pickup/                  Store-employee landing
│       │   ├── sales/import/
│       │   ├── import/
│       │   ├── analytics/
│       │   ├── command-center/
│       │   ├── advanced/
│       │   ├── reports/
│       │   ├── shipping/
│       │   ├── taxes/
│       │   ├── time-tracking/
│       │   ├── water-log/  (sub-tree)
│       │   ├── wholesale/
│       │   └── launch-checklist/
│       │
│       ├── wholesale/         (separate sub-app, not under admin/layout)
│       │   ├── login/page.tsx
│       │   ├── portal/page.tsx
│       │   ├── register/page.tsx
│       │   ├── employee/page.tsx
│       │   └── payment/{success,cancel}/page.tsx
│       │
│       ├── water/             (separate sub-app, not under admin/layout)
│       │   ├── page.tsx
│       │   ├── loading.tsx
│       │   └── admin/  (login + page)
│       │
│       ├── ird/time-clock/    (time-clock for IRD employees)
│       ├── trace/[lotNumber]/page.tsx     (public route-trace lookup)
│       └── protected-example/page.tsx     (smoke test for middleware)

Key nesting rules

  1. The root app/layout.tsx is the only one that renders <html> and <body>. Every other layout is a child of it.
  2. A layout.tsx in a folder wraps every page.tsx in that folder and all subfolders. Children swap; layout state persists.
  3. loading.tsx and error.tsx are per-folder — Next.js shows them when navigating to a sibling page.tsx. They re-render as you move between sections.
  4. Storefronts are fully isolated: each tuxedo/layout.tsx and indian-river-direct/layout.tsx provides its own backdrop and lets individual pages render their own StorefrontHeader / StorefrontFooter.
  5. Admin is a single SPA-feeling shell: navigating between /admin/orders and /admin/products keeps the AdminSidebar mounted, only the content area swaps.

Things that DO NOT yet nest (gaps)

Page Issue Fix
/cart No shared layout with /checkout Add app/(shop)/layout.tsx route group to wrap both
/wholesale/* Wholesale pages have no shared layout Add app/wholesale/layout.tsx with auth gate + nav
/water/* Water pages have no shared layout Add app/water/layout.tsx
/admin/communications/* 13 pages share no sub-layout Could add app/admin/communications/layout.tsx with section tabs
/admin/settings/* 13 settings pages share no sub-layout Add app/admin/settings/layout.tsx with settings sidebar

How to add a nested layout

# Example: add a settings sub-layout
mkdir -p src/app/admin/settings
# Create src/app/admin/settings/layout.tsx
# It will wrap every page under /admin/settings/* automatically.
// src/app/admin/settings/layout.tsx
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="grid grid-cols-1 lg:grid-cols-[14rem_1fr] gap-6">
      <SettingsNav />     {/* sticks while content swaps */}
      <section>{children}</section>
    </div>
  );
}

How to add parallel routes (modals as overlays)

mkdir -p 'src/app/@modal'
mkdir -p 'src/app/admin/products/(.)new'   # intercepts /admin/products/new
# Layout returns <>{children}{modal}</>

This makes /admin/products/new open as a modal over /admin/products instead of a hard navigation.