Next.js Layout: 27 Routes, One Root Layout, and the One Reason a Second Exists
27 routes, 2 layout.tsx files, and 11 places that render the same header and footer independently — the one nested layout isn't for shared chrome.
A Next.js layout.tsx wraps the pages under a route segment, persists across navigation instead of remounting, and only re-renders when its own props change. This storefront has 27 routes and exactly 2 layout.tsx files — but not because 25 of those routes share one layout for their header and footer. They don't share a layout for that at all. The one nested layout this codebase has exists for a single, different reason: an auth check that has to run before any dashboard page renders, guaranteed by the file system rather than by a rule someone has to remember.
The count
$ find src/app -name "page.tsx" | wc -l
27
$ find src/app -name "layout.tsx"
src/app/layout.tsx
src/app/dashboard/layout.tsx
27 routes — the home page, the catalog and product pages, pricing, docs, the blog and its posts, legal pages, the seven pSEO framework spokes, auth pages, and the two-page dashboard. Two layouts. If the textbook "layouts share your header and footer" story were what's happening here, that ratio would be suspicious — 25 routes sharing chrome through exactly one root layout is normal; what's unusual is what the root layout actually contains.
What the root layout does not do
// src/app/layout.tsx (trimmed)
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: `try{if(localStorage.getItem('theme')==='dark'){…}}catch(e){}` }} />
</head>
<body className={`${outfit.variable} font-sans antialiased`}>
<a href="#main" className="sr-only …">Skip to main content</a>
<JsonLd data={organizationLd} />
{children}
<Script src="https://app.lemonsqueezy.com/js/lemon.js" strategy="afterInteractive" />
<Analytics />
</body>
</html>
);
}
No <Header />. No <Footer />. The root layout carries the <html>/<body> shell, the dark-mode flash-prevention script, site-wide Organization JSON-LD, the LemonSqueezy checkout script, and Vercel Analytics — cross-cutting concerns that belong exactly once, at the top of everything. It does not carry the navigation chrome every marketing page visibly shares.
That chrome is rendered by every page independently. HomeTemplate, PricingTemplate, DocsTemplate and the seven other Atomic Design template components each import and render their own <Header /> and <Footer /> — even src/app/login/page.tsx, which isn't wrapped in a template component at all, does the same directly:
// src/components/templates/HomeTemplate.tsx
export default function HomeTemplate() {
return (
<>
<Header />
<main id="main">
<Hero />
{/* … */}
</main>
<Footer />
</>
);
}
Eleven separate files render the identical <Header />/<Footer /> pair. Not one of them inherits it from a shared layout.
Folklore vs. what this codebase's one nested layout is actually for
| What most Next.js layout tutorials teach | What dashboard/layout.tsx is actually for here | |
|---|---|---|
| Typical use case | Share a header/footer/sidebar across a route group | Run one piece of logic before any child renders |
| What breaks without it | Repeated markup (a maintenance annoyance) | A private page rendering with no auth check at all |
| Where the guarantee lives | Convention — every page remembers to include the shared chrome | The file system — every current and future file under /dashboard passes through it, unconditionally |
The chrome-sharing use case is real and common — it's just not why this project's second layout exists. Here, an auth gate is the one thing that has to be true for every file that will ever live under /dashboard, including ones that don't exist yet:
// src/app/dashboard/layout.tsx
export default async function DashboardLayout({ children }: { children: ReactNode }) {
const supabase = await createClient();
const { data } = await supabase.auth.getClaims();
if (!data?.claims) {
redirect("/login?next=/dashboard");
}
return (
<>
<Header />
<main id="main">
<section className="pt-30 pb-16 md:pb-24">
<Container>
<div className="mx-auto max-w-[1200px]">{children}</div>
</Container>
</section>
</main>
<Footer />
</>
);
}
If /dashboard/settings (or any future /dashboard/* page) instead had to remember to call getClaims() and redirect itself — the way every other route composes its own template — one omission ships an unauthenticated view of private account data. A layout.tsx is the one Next.js primitive where that can't happen by omission: the file sits between the route and every one of its children in the file tree, so it runs whether or not the page below it remembers anything about auth. That's a guarantee composition alone can't give you, which is exactly why this is the one place the codebase reaches for it.
Why Header/Footer aren't hoisted the same way
The honest answer is that hoisting them would have worked too — every one of the 27 routes wants the same header and footer, which is the textbook case for one shared layout. This codebase instead treats "the page's full shell" as something each Atomic Design template owns (HomeTemplate, PricingTemplate, and so on render their own <Header />/<Footer /> alongside their content, by convention — see CLAUDE.md's Atomic Design table), and there's no persisted-across-navigation state in Header that would make sharing one layout instance pay for its complexity: the mobile menu's open/closed state is local useState, reset on every navigation regardless of whether the header itself remounts. Duplicated markup with no shared state to lose is a cost layout.tsx doesn't need to solve here — the dashboard's auth check is a cost only layout.tsx can solve.
When to reach for a nested layout, and when not to
- Reach for one when something has to be guaranteed for an entire subtree regardless of which file renders — an auth check, a subscription gate, a required data fetch every child depends on.
- Don't reach for one just because several pages happen to share markup. If nothing needs to survive a navigation (open state, in-flight data, scroll position), a shared component each page renders costs nothing extra and keeps each page's composition visible in that page's own file — which is the same trade-off Atomic Design already makes at the organism/template level.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| A new page under a protected route renders with no auth check | The check lived in the page, not a layout, and this page forgot to call it | Move the check into a layout.tsx at the top of that subtree so it can't be skipped |
| Shared header state (e.g. a mobile menu) doesn't persist across navigation | Each page/template renders its own <Header /> instance instead of one shared via a layout | Hoist the component into a layout.tsx if persisted state across navigation is actually needed — duplication is fine if it isn't |
| A layout's data fetch re-runs on every navigation when it shouldn't | Next.js caches a layout's render per navigation only while its own segment is active; a wrapping route change can still re-trigger it | Cache the underlying fetch, not just rely on the layout not re-rendering |
| Root layout content appears on a route that shouldn't have it (e.g. Analytics loading on a 404) | Root layout wraps error.tsx/not-found.tsx too — there's no opt-out short of restructuring | Confirm the effect is actually appropriate site-wide before adding it to the root layout at all |
A layout.tsx accidentally makes an entire subtree client-rendered | A "use client" directive near the top of the tree forces everything under it into the client bundle | Keep layouts as Server Components (as dashboard/layout.tsx is) unless they specifically need client-only APIs |
Frequently asked questions
Is a layout.tsx the same as a Next.js app layout template component?
No, and this codebase has both, doing different jobs: a layout.tsx is a framework file-convention that wraps a route segment and persists across navigation within it. This project's own "template" components (HomeTemplate, PricingTemplate, …) are an Atomic Design layer — plain React components a page.tsx renders once, with no persistence guarantee and no relationship to the file-system routing convention.
What's a minimal Next.js layout example?
The root layout above, trimmed further, is close to the minimum: a component accepting children, returning <html>/<body>, with the child content rendered inside. Next.js requires the root layout specifically to include <html> and <body>; a nested layout like dashboard/layout.tsx doesn't repeat those tags — it just returns whatever wrapper markup that subtree needs around {children}.
Do I need a layout for every route segment?
No — a segment without its own layout.tsx simply renders inside the nearest ancestor layout, all the way up to the root. Of this project's 27 routes, 25 have no layout of their own at all; only the root and the dashboard subtree needed one.
Can a layout fetch data, or should that stay in the page?
A layout can fetch data (it's a Server Component by default), and that's appropriate when every child under it needs that same data — a subscription check, in this codebase's case, though dashboard/layout.tsx re-derives the claims from the request rather than fetching separately. For data specific to one page, fetching in the page keeps that dependency visible where the page is read, rather than implied by which layout happens to wrap it.
Templates in this post
ASoc Ignite (an AI app landing page), ASoc Iris (a computer-vision AI product site) and ASoc Keystone (a mortgage-lender website) all follow the same Atomic Design template layer audited above.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates. For the auth check this codebase's one nested layout actually runs, see Supabase Auth in Next.js 16; for the component layer that replaces per-route layouts here, Atomic design in Next.js.
