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.
13 KiB
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
- The root
app/layout.tsxis the only one that renders<html>and<body>. Every other layout is a child of it. - A
layout.tsxin a folder wraps everypage.tsxin that folder and all subfolders. Children swap; layout state persists. loading.tsxanderror.tsxare per-folder — Next.js shows them when navigating to a siblingpage.tsx. They re-render as you move between sections.- Storefronts are fully isolated: each
tuxedo/layout.tsxandindian-river-direct/layout.tsxprovides its own backdrop and lets individual pages render their ownStorefrontHeader/StorefrontFooter. - Admin is a single SPA-feeling shell: navigating between
/admin/ordersand/admin/productskeeps theAdminSidebarmounted, 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.