9458fd0506
Deploy to route.crispygoat.com / deploy (push) Successful in 4m20s
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.
256 lines
13 KiB
Markdown
256 lines
13 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
# 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.
|
|
```
|
|
|
|
```tsx
|
|
// 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)
|
|
|
|
```bash
|
|
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.
|