Skip to main content
ASoc
Tutorial

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.

The ASoc Team8 min read

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 teachWhat dashboard/layout.tsx is actually for here
Typical use caseShare a header/footer/sidebar across a route groupRun one piece of logic before any child renders
What breaks without itRepeated markup (a maintenance annoyance)A private page rendering with no auth check at all
Where the guarantee livesConvention — every page remembers to include the shared chromeThe 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

SymptomCauseFix
A new page under a protected route renders with no auth checkThe check lived in the page, not a layout, and this page forgot to call itMove 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 navigationEach page/template renders its own <Header /> instance instead of one shared via a layoutHoist 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'tNext.js caches a layout's render per navigation only while its own segment is active; a wrapping route change can still re-trigger itCache 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 restructuringConfirm 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-renderedA "use client" directive near the top of the tree forces everything under it into the client bundleKeep 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.

Keep reading

Tutorial9 min read

Next.js Link: 40 of Them, and 22 CTAs the Lint Rule Cannot See

no-html-link-for-pages reads literal hrefs on raw anchors, so it never saw this site's button atom. Plus the internal URL that must stay an anchor: /api/download.

Read more
Tutorial11 min read

Persisting UI State in localStorage Without a Hydration Mismatch

Prerendered HTML cannot know what one browser saved. An empty server snapshot, the getSnapshot cache that stops an infinite render loop, and why theme is the opposite problem.

Read more