Home JavaScript Nested Layouts in Next.js 16 — Missing layout.js Caused 404 on Every Sub-Page
Beginner 11 min · July 12, 2026
Pages and Layouts in Next.js: File-Based Routing Explained

Nested Layouts in Next.js 16 — Missing layout.js Caused 404 on Every Sub-Page

Your layout.js is missing and every sub-page returns 404.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.

Follow
Production
production tested
July 12, 2026
last updated
1,787
articles · all by Naren
Before you start⏱ 15 min
  • Solid grasp of React fundamentals (components, props, state)
  • Node.js 18+ installed
  • Basic understanding of Next.js concepts (Server Components, App Router)
  • Familiarity with the command line
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • The filesystem IS the router — app/blog/page.tsx = /blog, no config files needed
  • Every route segment needs a page.js — a layout.js alone is not enough
  • Layouts persist across navigations and do not re-render; templates re-render on every navigation
  • Missing layout.js at any level causes 404s on all child routes — Next.js requires a layout chain from root to leaf
✦ Definition~90s read
What is Pages and Layouts in Next.js?

The Next.js App Router is a filesystem-based routing system where the folder structure defines the URL structure. Unlike traditional routers (React Router, Vue Router) that use a configuration file, the App Router maps files and folders directly to URL paths. A file at app/blog/[slug]/page.tsx automatically creates the route /blog/:slug — no config, no registration, no boilerplate.

Think of Next.js's filesystem router like a filing cabinet.

The App Router supports layouts (persistent UI wrappers), templates (re-rendering wrappers), loading states, error boundaries, parallel routes (multiple independent pages in one layout), and intercepting routes (modals that show content without navigating). It's built on top of React Server Components, which means pages can be async and fetch data directly without client-side JavaScript.

The key insight: the filesystem is not a suggestion — it's the contract. Every folder adds a URL segment. Every page.js creates a route. Every layout.js wraps its children. Understanding this hierarchy is the foundation of building with Next.js.

Plain-English First

Think of Next.js's filesystem router like a filing cabinet. Each drawer (folder) is a URL segment. Each file inside is a page. The layout.js files are like the drawer's label and dividers — they organize the content but don't replace the actual documents. If you remove a divider (layout), the drawer still exists, but the documents (pages) inside become inaccessible. The filing cabinet doesn't use a map or index — the physical arrangement of folders IS the map.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

You created app/blog/page.tsx and expected /blog to work. It returned 404. You checked the file name, the folder name, the casing — everything looks right. But Next.js is telling you the route doesn't exist. Welcome to the filesystem router, where the folder structure IS the routing configuration.

If you're coming from React Router, this is the biggest mental shift. React Router uses a config file where you declare routes: <Route path="/blog" element={<Blog />} />. Next.js uses the filesystem: app/blog/page.tsx = /blog. No config, no registration, no BrowserRouter. The folder hierarchy IS the route hierarchy.

This article covers everything about the App Router: layouts, templates, nested routes, dynamic segments, route groups, parallel routes, and the common pitfalls that cause 404s. By the end, you'll understand why the filesystem router is both more powerful and more restrictive than React Router — and how to use it without fighting it.

The Filesystem IS the Router — Unlearn Everything You Know

If you're coming from React Router, you're used to this: a routes.js file where you declare <Route path="/blog" element={<Blog />} />. Next.js doesn't work that way. The filesystem IS the router. app/blog/page.tsx maps to /blog. app/blog/[slug]/page.tsx maps to /blog/:slug. There is no route config file. There is no BrowserRouter wrapper. The folder structure IS the route structure.

This is a hard mental shift. React Router treats routes as data — you can map, filter, and compose them programmatically. Next.js treats routes as files — you create, delete, and move them with the filesystem. You can't dynamically generate routes at runtime (without generateStaticParams). You can't wrap routes with HOCs in a config file. The filesystem is the source of truth.

The benefit: zero boilerplate. No route config, no BrowserRouter, no Switch component. Create a file, get a route. The cost: less flexibility. You can't programmatically add routes at runtime. You can't use regex patterns in paths. You can't nest routes arbitrarily without creating folders.

app/layout.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Root layout — wraps every page in the app
// Required. Without it, every route returns 404.

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <header>Site Header</header>
        {children}
        <footer>Site Footer</footer>
      </body>
    </html>
  )
}
Try it live
Folders = URL segments, Files = UI
app/dashboard/settings/page.tsx → /dashboard/settings. The folder IS the path.
Production Insight
A team migrating from React Router tried to keep their old route config and map it to files. They created 47 folders with empty page.tsx files that just re-exported components from a central router. They lost all the benefits of code splitting and lazy loading. The fix: delete the central router and let each page.tsx import its own components.
Key Takeaway
- The filesystem IS the router — no config file, no BrowserRouter, no route registration
- Every folder in app/ adds a URL segment — app/blog/[slug]/page.tsx/blog/:slug
- Layouts persist across navigations — they don't re-render when the child page changes
- Templates re-render on every navigation — use them for animations or analytics
nextjs-pages-layouts-routing THECODEFORGE.IO Next.js 16 Nested Layout Architecture Layered UI wrappers and route hierarchy Root Layout layout.js (global) | Header | Footer Route Group Layer (marketing) | (dashboard) Segment Layout layout.js (per segment) | Sidebar | Nav Template Layer template.js (re-render) | Dynamic content Parallel Routes @analytics | @team | Slot UI Error & Loading UI error.js | loading.js | Fallback states THECODEFORGE.IO
thecodeforge.io
Nextjs Pages Layouts Routing

Layouts — The Persistent UI Wrapper

A layout.js file wraps its child pages and nested layouts. It persists across navigations — when the user navigates from /blog to /blog/hello-world, the layout does NOT re-render. Only the page.tsx content changes. This is the key performance feature of layouts: shared UI (headers, footers, sidebars) renders once and stays mounted.

Layouts can be nested. app/layout.js wraps everything. app/blog/layout.js wraps all blog pages. app/blog/[slug]/layout.js wraps individual blog posts. Each layout adds a wrapper around its children. The nesting is automatic — you don't need to configure it.

But there's a critical rule: layouts don't re-render when the child page changes. If you have a sidebar with navigation state, that state persists across page navigations. This is usually what you want — but it can cause bugs if the layout depends on data that changes between pages. In that case, use a template.js instead (it re-renders on every navigation).

app/blog/layout.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Blog layout — wraps all blog pages
// This layout persists across /blog, /blog/[slug], etc.
// It does NOT re-render when navigating between blog posts

export default function BlogLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="max-w-4xl mx-auto">
      <nav>
        <a href="/blog">All Posts</a>
      </nav>
      <main>{children}</main>
    </div>
  )
}
Try it live
Layouts persist, templates re-render
Layouts keep state across navigations. Templates reset state on every page change.
Production Insight
A team used a layout with a sidebar that fetched the current user's data. When navigating between pages, the sidebar showed stale data because the layout didn't re-render. The fix: move user data fetching to a template.js (which re-renders on every navigation) or use a client component with useEffect to refetch on route change.
Key Takeaway
- Layouts persist across navigations — they don't re-render when the child changes
- Templates re-render on every navigation — use them for animations, analytics, or per-page state
- Layouts can be nested — each folder can have its own layout.js
- The root layout.js is required — it must contain <html> and <body> tags

Templates — When Layouts Are Not Enough

Templates (template.js) are like layouts, but they re-render on every navigation. If a layout persists state (e.g., a sidebar with scroll position), a template resets it. Use templates when: you need to run an effect on every page view (analytics, page view tracking), you want to reset state between pages, or you're animating page transitions.

The performance trade-off: templates re-render their children on every navigation, which means React unmounts and remounts the component tree. This is more expensive than a layout, which keeps the tree mounted. Use layouts by default, templates only when you need the reset behavior.

Common use case: page view tracking. If you put analytics in a layout, it fires once when the layout mounts. If the user navigates to another page in the same layout, the analytics event doesn't fire again. Put analytics in a template to fire on every page view.

app/blog/template.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Template — re-renders on every navigation
// Use for: page view tracking, animations, resetting state
'use client'

export default function BlogTemplate({
  children,
}: {
  children: React.ReactNode
}) {
  // This effect runs on every page navigation
  useEffect(() => {
    analytics.pageView(window.location.pathname)
  }, [])
  
  return <div className="animate-fadeIn">{children}</div>
}
Try it live
Analytics go in templates, not layouts
Layouts don't re-render on navigation. Templates do. Put page view tracking in template.js.
Production Insight
A team put Google Analytics in their root layout. They wondered why page views were underreported by 80%. The layout only fired the analytics event once — when the app first loaded. Navigating between pages didn't trigger new events. Moving the analytics script to a template.js fixed the reporting.
Key Takeaway
- Layouts persist across navigations — they don't re-render when the child changes
- Templates re-render on every navigation — use them for analytics, animations, or per-page state
- Nested layouts compose automatically — each folder can have its own layout.js
- The root layout must contain <html> and <body> tags — only the root layout should have these
Layout.js vs Template.js in Next.js 16 Persistent wrappers vs re-rendering components layout.js template.js State Persistence Preserved across navigation Reset on each route change Re-render Behavior Mounted once, not re-rendered Re-mounted for every sub-page Use Case Persistent UI like sidebars Dynamic content like analytics Missing File Impact Causes 404 on nested routes No 404, but no wrapper Performance Lower overhead (cached) Higher overhead (re-render) THECODEFORGE.IO
thecodeforge.io
Nextjs Pages Layouts Routing

Route Groups — Organizing Without Affecting URLs

Route groups are folders wrapped in parentheses: (marketing), (dashboard), (auth). They don't affect the URL path. app/(marketing)/about/page.tsx maps to /about, not /(marketing)/about. This is useful for: organizing routes by feature, applying different layouts to different sections, and separating public vs authenticated routes.

A common pattern: app/(public)/layout.tsx for the marketing site layout, app/(dashboard)/layout.tsx for the authenticated app layout. Both layouts can have different headers, footers, and navigation. The routes under each group share the same layout without affecting the URL.

Route groups can be nested: app/(marketing)/blog/[slug]/page.tsx still maps to /blog/:slug. The (marketing) group doesn't appear in the URL. This is purely organizational.

app/(dashboard)/layout.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Dashboard layout — wraps all dashboard routes
// The (dashboard) group doesn't appear in the URL
// /dashboard/settings still works as expected

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="flex h-screen">
      <aside className="w-64 bg-gray-100">
        <nav>
          <a href="/dashboard">Overview</a>
          <a href="/dashboard/settings">Settings</a>
          <a href="/dashboard/analytics">Analytics</a>
        </nav>
      </aside>
      <main className="flex-1">{children}</main>
    </div>
  )
}
Try it live
Route groups don't affect URLs
app/(marketing)/about/page.tsx → /about. The (marketing) group is invisible in the URL.
Production Insight
A team had a single layout for both public pages and the admin dashboard. The admin sidebar was visible on the public homepage. They split routes into (public) and (admin) route groups with different layouts. The public layout had a marketing header; the admin layout had a sidebar. Same URL structure, different layouts.
Key Takeaway
- Route groups (group) organize routes without affecting URLs
- Use different route groups for different layouts (public vs authenticated)
- Route groups can be nested — (marketing)/blog and (dashboard)/blog are different routes
- Route groups are invisible in the URL — they're purely organizational

Dynamic Routes — [slug], [...slug], and [[...slug]]

Dynamic routes use bracket syntax in folder names. [slug] matches a single segment: /blog/hello-worldparams.slug = "hello-world". [...slug] matches one or more segments: /blog/2024/01/helloparams.slug = ["2024", "01", "hello"]. [[...slug]] matches zero or more segments: /blogparams.slug = undefined, /blog/helloparams.slug = ["hello"].

The params prop is a Promise in Next.js 16 — you must await it. This is a change from Next.js 15 where params was a synchronous object. The async params enables better streaming and partial rendering.

Dynamic routes can be pre-rendered at build time using generateStaticParams. This function returns an array of param objects, and Next.js generates static HTML for each one. For a blog with 1,000 posts, generateStaticParams creates 1,000 static HTML files at build time.

app/blog/[[...slug]]/page.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Catch-all route: matches /blog, /blog/hello, /blog/2024/01/post
// [[...slug]] matches zero or more segments

export default async function BlogPage({
  params,
}: {
  params: Promise<{ slug?: string[] }>
}) {
  const { slug } = await params
  
  if (!slug) {
    // /blog — show all posts
    return <PostList />
  }
  
  // /blog/2024/01/post-slug
  const [year, month, postSlug] = slug
  return <PostDetail slug={postSlug} />
}
Try it live
params is a Promise in Next.js 16
Always await params in your page components. It's async in Next.js 16.
Production Insight
A team used [[...slug]] for their catch-all route expecting it to match /blog (zero segments). But they also had app/blog/page.tsx which created a conflict. Next.js prioritized the explicit page.tsx over the catch-all. The fix: remove the explicit page.tsx and handle the root case inside the catch-all component.
Key Takeaway
- [slug] matches one segment, [...slug] matches one or more, [[...slug]] matches zero or more
- Explicit routes take priority over dynamic routes — app/blog/page.tsx beats app/blog/[slug]/page.tsx
- params is a Promise in Next.js 16 — always await it
- Use generateStaticParams to pre-render dynamic routes at build time

Parallel Routes — Multiple Pages in One Layout

Parallel routes let you render multiple pages in the same layout simultaneously. They use @slot folders: app/@feed/page.tsx and app/@sidebar/page.tsx render independently in the same layout. The layout receives both slots as props and can arrange them however it wants.

This is useful for dashboards with independent sections. A dashboard might have a main feed, a sidebar with notifications, and a header with user info — all rendering independently. Each slot has its own loading and error states. If the feed fails to load, the sidebar still works.

Parallel routes also enable conditional rendering based on route. You can show different slots for different authentication states. For example, app/@login/page.tsx for unauthenticated users and app/@dashboard/page.tsx for authenticated users — both at the same URL.

app/@feed/page.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Parallel route slot — renders independently from @sidebar
// This slot has its own loading state and error boundary

export default async function Feed() {
  const posts = await fetch('https://api.example.com/feed')
    .then(res => res.json())
  
  return (
    <div className="feed">
      {posts.map((post: any) => (
        <article key={post.id}>{post.title}</article>
      ))}
    </div>
  )
}
Try it live
Parallel routes = independent UI sections
Use @feed, @sidebar, @header slots to render independent UIs in the same layout.
Production Insight
A team built a dashboard with a feed, sidebar, and header as parallel routes. When the feed API was slow, the sidebar and header still rendered instantly. Each slot had its own loading state. Users saw the sidebar immediately while the feed loaded. This pattern cut perceived load time by 60%.
Key Takeaway
- Parallel routes use @slot folders — each slot renders independently
- Each slot has its own loading.js and error.js — failures in one slot don't affect others
- Parallel routes are great for dashboards with independent UI sections
- Use default.js in each slot to handle the case when no page matches

Intercepting Routes — Modals and Drill-Downs

Intercepting routes let you show a route's content in a modal while keeping the underlying page in the background. They use (..) prefix syntax: app/feed/(..)photo/[id]/page.tsx intercepts the /photo/:id route when navigated to from the feed.

The intercept levels: (.) matches the same level, (..) matches one level up, (..)(..) matches two levels up, (...) matches from the root app directory. This is useful for photo galleries, modals, and drill-down navigation where you want to show content in a modal without losing the underlying page.

When the user navigates directly to the URL (not from the feed), the intercept is bypassed and the normal page renders. This gives you the best of both worlds: smooth in-app navigation with modals, and direct URL access with full pages.

app/feed/(..)photo/[id]/page.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Intercepting route: /photo/123 when navigated from /feed
// Shows photo in a modal instead of navigating to /photo/123
// Direct access to /photo/123 shows the full page (not intercepted)

export default async function PhotoModal({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const photo = await getPhoto(id)
  
  return (
    <div className="fixed inset-0 bg-black/50 flex items-center justify-center">
      <div className="bg-white rounded-lg p-4">
        <Image src={photo.url} alt={photo.title} width={800} height={600} />
        <button onClick={() => router.back()}>Close</button>
      </div>
    </div>
  )
}
Try it live
Intercepting routes = modals
Use (..)photo/[id] to show photos in a modal from the feed, but as a full page when accessed directly.
Production Insight
A photo gallery app used intercepting routes for the lightbox. When users clicked a photo from the feed, it opened in a modal (intercepting route). When users shared the URL, it opened as a full page. The intercept used (..)photo/[id] — one level up from the feed. This pattern is used by Instagram, Twitter, and most social platforms.
Key Takeaway
- Intercepting routes show content in a modal when navigated from within the app
- Direct URL access bypasses the intercept and shows the full page
- Use (.) for same level, (..) for one level up, (...) for root level
- This pattern is used by Instagram, Twitter, and most social platforms for photo modals

Loading and Error States — The Parallel UI Contract

Every route segment can have loading.js and error.js files. loading.js shows a fallback UI while the page is loading (streaming or data fetching). error.js catches errors in the page and shows a fallback. Both are optional — if you don't provide them, Next.js uses default behavior (blank page for loading, error page for errors).

loading.js wraps the page in a React Suspense boundary. It shows immediately while the page content streams in. This is critical for perceived performance — users see a skeleton or spinner instead of a blank screen. The loading state is shown for the first render and for any Suspense boundaries that are still streaming.

error.js is an error boundary. It catches errors thrown during rendering, data fetching, or in client components. It receives the error and a reset function to retry. Error boundaries are scoped to their segment — an error in app/blog/[slug]/page.tsx shows the error UI for that post, not the entire app.

app/blog/loading.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
// Loading state for /blog and /blog/[slug]
// Shows immediately while the page content streams in

export default function BlogLoading() {
  return (
    <div className="animate-pulse space-y-4">
      <div className="h-8 bg-gray-200 rounded w-1/3" />
      <div className="h-4 bg-gray-200 rounded w-2/3" />
      <div className="h-4 bg-gray-200 rounded w-1/2" />
      <div className="h-64 bg-gray-200 rounded" />
    </div>
  )
}
Try it live
Always add loading.js
A loading.js skeleton improves perceived performance even if the actual load time is the same.
Production Insight
A team's dashboard had a 3-second data fetch. Without loading.js, the page was blank for 3 seconds. Users thought the app was broken. Adding a skeleton loading state reduced bounce rate by 40% — even though the actual load time didn't change.
Key Takeaway
- loading.js shows a fallback while the page content streams in
- error.js catches errors and shows a fallback with a retry button
- Both are scoped to their segment — errors in one page don't crash the entire app
- Always add loading.js for pages with data fetching — it improves perceived performance

Metadata — SEO and Social Cards

Next.js has a built-in metadata API for SEO. Export a metadata object or generateMetadata function from any layout.js or page.js. The metadata is automatically injected into the <head> of the page — title, description, Open Graph, Twitter cards, and more.

Static metadata: export a metadata object with title, description, openGraph, and twitter fields. Dynamic metadata: export a generateMetadata function that receives params and searchParams and returns the metadata object. This is useful for blog posts where the title and description come from the database.

Metadata merges from parent to child. The root layout sets the base title and description. Child pages can override or extend them. If a child page doesn't export metadata, the parent's metadata is used. This is the same pattern as layouts — parent provides defaults, children override.

app/blog/[slug]/page.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import type { Metadata } from 'next'

// Dynamic metadata — generated from the blog post data
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>
}): Promise<Metadata> {
  const { slug } = await params
  const post = await getPost(slug)
  
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.ogImage],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
    },
  }
}

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await getPost(slug)
  return <article>{post.content}</article>
}
Try it live
Metadata merges from parent to child
Root layout sets the base title. Child pages can override it. No override = parent's metadata.
Production Insight
A team's blog posts all showed the same title in Google Search results. They forgot to export generateMetadata from their blog post pages. Google indexed every post with the root layout's title. After adding dynamic metadata, organic traffic increased by 300% in 2 weeks.
Key Takeaway
- Export metadata for static SEO data, generateMetadata for dynamic data
- Metadata merges from parent to child — child overrides parent
- Always set title and description for every page — they're critical for SEO
- Use generateMetadata for blog posts, product pages, and any dynamic content

The 404 Bug — Why Missing layout.js Breaks Everything

The most common routing bug in Next.js: you create app/dashboard/settings/page.tsx but /dashboard/settings returns 404. The cause is almost always a missing layout.js in a parent segment. Next.js requires a layout chain from the root to every leaf page. If any segment in the chain is missing a layout, the route returns 404.

This is because Next.js wraps every page in its parent layouts. The rendering chain is: root layout → segment layout → segment layout → page. If any link in that chain is missing, Next.js can't render the page. The error message is misleading — it says "404" but the real issue is a missing layout, not a missing page.

To debug: check every folder in the path from root to the page. Each folder should have either a layout.js or be a leaf with a page.js. If a folder has neither, routes under it will 404. The exception is route groups (group) — they don't need their own layout (they inherit from the parent).

app/error.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
'use client'

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div className="flex flex-col items-center justify-center min-h-[400px]">
      <h2>Something went wrong!</h2>
      <p className="text-gray-500">{error.message}</p>
      <button
        onClick={() => reset()}
        className="mt-4 px-4 py-2 bg-blue-500 text-white rounded"
      >
        Try again
      </button>
    </div>
  )
}
Try it live
Missing layout = 404 on all child routes
Every segment needs a layout.js or page.js. A missing layout breaks all routes under it.
Production Insight
A team deleted app/(dashboard)/layout.js thinking it was unused because the dashboard still rendered. But all sub-routes (/dashboard/settings, /dashboard/analytics) returned 404. The root layout was wrapping the dashboard page directly, skipping the dashboard layout. The sub-routes couldn't render because they had no layout chain.
Key Takeaway
- Every segment needs a layout.js or page.js — missing layout = 404 on child routes
- The root layout is required — it must contain <html> and <body>
- Layouts can be nested infinitely — each folder can have its own layout.js
- Use route groups to organize layouts without affecting URLs

The Pages Router — Legacy Routes and Migration

Next.js supports two routers: the App Router (app/) and the Pages Router (pages/). The Pages Router is the legacy system from Next.js 12 and earlier. It uses pages/index.js for /, pages/blog/[slug].js for /blog/:slug, and pages/api/ for API routes. It does NOT support layouts, Server Components, or streaming.

You can use both routers in the same project. Routes in app/ take priority over routes in pages/. If you have app/about/page.tsx and pages/about.js, the App Router version wins. This is useful for incremental migration — you can move routes from Pages Router to App Router one at a time.

But there's a catch: the Pages Router doesn't support layouts. If you have a pages/_app.js with a custom layout, it won't work with App Router pages. You need to migrate the layout to app/layout.js and move the pages to app/ as well. You can't mix layouts between the two routers.

app/blog/[slug]/page.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import type { Metadata } from 'next'

export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>
}): Promise<Metadata> {
  const { slug } = await params
  const post = await getPost(slug)
  
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [{ url: post.ogImage }],
    },
  }
}

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await getPost(slug)
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  )
}
Try it live
Pages Router and App Router can coexist
But layouts don't cross over. App Router pages can't use Pages Router _app.js layout.
Production Insight
A team migrated their pages one by one from Pages Router to App Router. They kept pages/_app.js for the old pages and app/layout.js for the new ones. The migration took 3 months with zero downtime. Each page was migrated independently, tested, and deployed.
Key Takeaway
- App Router and Pages Router can coexist in the same project
- App Router routes take priority over Pages Router routes for the same path
- Migrate pages one at a time — you don't need to do it all at once
- Pages Router doesn't support layouts, Server Components, or streaming

The 404 Bug Deep Dive — Debugging Missing Layouts

When a route returns 404 and you've confirmed the file exists, the most likely cause is a missing layout.js in a parent segment. Next.js builds a layout tree from root to leaf. If any segment in the tree is missing a layout, the entire branch returns 404.

To debug: start at the route that 404s and walk up the folder hierarchy. app/dashboard/settings/page.tsx needs: app/layout.js (root), app/dashboard/layout.js (or app/(dashboard)/layout.js), and app/dashboard/settings/page.tsx. If any of those is missing, the route 404s.

The exception: route groups (group) don't need their own layout. If you have app/(marketing)/about/page.tsx, you only need app/layout.js (root) and app/(marketing)/about/page.tsx. The (marketing) group inherits the root layout unless you add a layout.js inside it.

app/layout.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Root layout — REQUIRED
// Without this file, every route in the app returns 404

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}
Try it live
404 ≠ missing page
A 404 often means a missing layout.js in a parent segment, not a missing page.js.
Production Insight
A team spent 3 hours debugging a 404 on /dashboard/settings. The file existed. The folder existed. The route was correct. The cause: they had app/(dashboard)/layout.js but deleted app/layout.js (the root layout). The root layout is required — without it, no route works. Restoring app/layout.js fixed all 404s.
Key Takeaway
- A 404 on a route that exists is almost always a missing parent layout
- The root layout is required — it must contain <html> and <body>
- Walk up the folder hierarchy to find the missing layout
- Route groups don't need their own layout — they inherit from the parent

The Template vs Layout Decision — When to Use Each

The decision between layout.js and template.js comes down to one question: do you want the UI to persist across navigations? If yes, use a layout. If no, use a template.

Layouts are for: navigation bars, sidebars, footers, and any UI that should stay mounted when the user navigates. Templates are for: page view analytics, entrance animations, scroll-to-top behavior, and any UI that should reset on every navigation.

There's a performance cost to templates. Because they re-render on every navigation, React unmounts and remounts the component tree. This means all state is lost — useState, useReducer, and any DOM state (scroll position, input values) are reset. If you need to preserve state, use a layout. If you need to reset state, use a template.

app/blog/template.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
'use client'

import { useEffect } from 'react'

export default function BlogTemplate({
  children,
}: {
  children: React.ReactNode
}) {
  // Runs on every navigation — not just first mount
  useEffect(() => {
    window.scrollTo(0, 0)
    analytics.pageView()
  }, [])
  
  return <div className="animate-fadeIn">{children}</div>
}
Try it live
Layouts for persistence, templates for freshness
Layouts keep state. Templates reset state. Choose based on whether you want persistence.
Production Insight
A team used a template for their dashboard sidebar because they wanted the sidebar to update when the user navigated. But the sidebar re-fetched data on every navigation, causing unnecessary API calls. They switched to a layout with a client component that used useEffect to refetch only when the user data changed. API calls dropped by 80%.
Key Takeaway
- Layouts persist state across navigations — use for headers, footers, sidebars
- Templates reset state on every navigation — use for analytics, animations, scroll-to-top
- Templates are more expensive than layouts — they unmount and remount the component tree
- You can have both layout.js and template.js in the same folder — template renders inside layout

Next.js's Link component enables client-side navigation. When you use <Link href="/about">About</Link>, Next.js prefetches the linked page (if it's in the viewport) and navigates without a full page reload. This is the difference between a SPA-like experience and traditional MPA navigation.

Link prefetches statically rendered pages by default. For dynamic pages, it prefetches the data. This means navigation feels instant — the JavaScript and data are already loaded before the user clicks. You can disable prefetching with prefetch={false} for rarely-used links.

But there's a trap: Link only works for client-side navigation. If you use an <a> tag instead of <Link>, you get a full page reload. This loses all client-side state, re-downloads the entire JavaScript bundle, and feels slow. Always use <Link> for internal navigation.

app/components/Navigation.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Link from 'next/link'

export default function Navigation() {
  return (
    <nav>
      <Link href="/">Home</Link>
      <Link href="/blog">Blog</Link>
      <Link href="/about">About</Link>
      <Link href="/contact">Contact</Link>
      
      {/* External link — use <a> */}
      <a href="https://example.com">External</a>
      
      {/* Disable prefetch for rarely-used links */}
      <Link href="/archive" prefetch={false}>
        Archive
      </Link>
    </nav>
  )
}
Try it live
Always use Link, not
<a> causes full page reloads. <Link> navigates client-side without reloading.
Production Insight
A team used <a> tags for all internal navigation because they were used to React Router's <Link>. Their app felt slow because every navigation caused a full page reload. Lighthouse score was 45 for performance. Switching to <Link> improved the score to 85 — navigation was instant.
Key Takeaway
- Use <Link> for internal navigation — it prefetches and navigates without full reload
- Use <a> for external links — they need full page navigation
- <Link> prefetches statically rendered pages by default
- Set prefetch={false} for rarely-used links to save bandwidth

The useRouter Hook — Programmatic Navigation

For programmatic navigation (after form submission, after login, etc.), use the useRouter hook from next/navigation. It provides push(), replace(), back(), forward(), and refresh(). push() adds to the history stack, replace() replaces the current entry, refresh() re-renders the current route without a full page reload.

useRouter is a client-side hook — it only works in Client Components ('use client'). For Server Components, use redirect() from next/navigation instead. redirect() throws a redirect error that Next.js catches and handles — it works in Server Components, Server Actions, and Route Handlers.

The refresh() method is unique to Next.js. It re-renders the current route without losing client-side state. This is useful after a mutation (e.g., creating a blog post) — you can refresh the page to show the new data without a full navigation.

app/components/LoginForm.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
'use client'

import { useRouter } from 'next/navigation'

export default function LoginForm() {
  const router = useRouter()
  
  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    
    const res = await fetch('/api/login', {
      method: 'POST',
      body: new FormData(e.target as HTMLFormElement),
    })
    
    if (res.ok) {
      router.push('/dashboard') // Navigate after login
      router.refresh() // Re-render server components
    }
  }
  
  return <form onSubmit={handleSubmit}>...</form>
}
Try it live
useRouter is client-side only
For Server Components, use redirect() from next/navigation instead of useRouter.
Production Insight
A team used router.push() in a Server Action to redirect after form submission. It didn't work because useRouter is a client-side hook. They switched to redirect() from next/navigation which works in Server Actions. The redirect happened before the response was sent — no client-side JavaScript needed.
Key Takeaway
- useRouter is client-side only — use redirect() in Server Components and Server Actions
- router.push() adds to history, router.replace() doesn't
- router.refresh() re-renders the current route without losing client state
- router.prefetch() manually prefetches a route (Link does this automatically)

The Root Layout — Why It's Required and What It Must Contain

The root layout.js is the only required file in a Next.js app (besides at least one page.js). It must contain <html> and <body> tags. Only the root layout should have these tags — nested layouts should not repeat them. If you add <html> in a nested layout, you'll get nested HTML tags in the rendered output, which is invalid HTML.

The root layout is where you import global CSS, set metadata defaults, and wrap the app with providers (theme, auth, etc.). It's the outermost shell of your application. Every page renders inside this layout.

If you delete the root layout, every route in the app returns 404. Next.js doesn't have a default layout — you must provide one. The scaffolded create-next-app creates it for you, but if you're starting from scratch or accidentally delete it, you need to recreate it.

app/layout.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import type { Metadata } from 'next'
import './globals.css'

export const metadata: Metadata = {
  title: {
    default: 'My App',
    template: '%s | My App', // Child pages: "About | My App"
  },
  description: 'Built with Next.js 16',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}
Try it live
Only root layout has and
Nested layouts should NOT contain <html> or <body>. Only the root layout has these.
Production Insight
A developer added <html> and <body> tags in a nested layout to override the lang attribute. The rendered HTML had nested <html> tags — <html><body><html><body>...</body></html></body></html>. Browsers handled it gracefully, but Lighthouse flagged it as invalid HTML. The fix: set the lang attribute in the root layout only.
Key Takeaway
- Only the root layout should contain <html> and <body> tags
- Nested layouts should only contain the UI wrapper (div, nav, aside)
- The root layout is required — without it, every route returns 404
- Import global CSS only in the root layout

Next.js's <Link> component is not the same as an <a> tag. It prefetches linked pages in the background (when the link is in the viewport), and navigates without a full page reload. This is the foundation of Next.js's fast navigation — pages feel instant because they're already loaded before the user clicks.

Prefetching behavior: statically rendered pages are fully prefetched (HTML + data). Dynamically rendered pages are prefetched only up to the loading boundary (the data is fetched on navigation). You can disable prefetching with prefetch={false} for links that are rarely used.

<Link> also supports scroll={false} to disable scroll-to-top on navigation. This is useful for paginated lists where you want to maintain scroll position. And replace to replace the current history entry instead of pushing a new one.

app/components/Pagination.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import Link from 'next/link'

export default function Pagination({ page, totalPages }: {
  page: number
  totalPages: number
}) {
  return (
    <div className="flex gap-2">
      <Link
        href={`/blog?page=${page - 1}`}
        scroll={false} // Don't scroll to top
        className={page <= 1 ? 'pointer-events-none opacity-50' : ''}
      >
        Previous
      </Link>
      
      <span>Page {page} of {totalPages}</span>
      
      <Link
        href={`/blog?page=${page + 1}`}
        scroll={false}
        prefetch={false} // Don't prefetch — user may not go next
      >
        Next
      </Link>
    </div>
  )
}
Try it live
Link prefetches in viewport
Links visible in the viewport are prefetched automatically. Navigation feels instant.
Production Insight
A team's navigation felt slow because they used <a> tags instead of <Link>. Every navigation caused a full page reload — 1.2 seconds of load time. Switching to <Link> made navigation instant (under 100ms) because pages were prefetched before the user clicked.
Key Takeaway
- <Link> prefetches pages in the viewport — navigation feels instant
- Use <a> for external links only — internal links should use <Link>
- <Link scroll={false}> disables scroll-to-top on navigation
- <Link prefetch={false}> disables prefetching for rarely-used links

The usePathname and useSearchParams Hooks

usePathname() returns the current URL pathname (e.g., /blog/hello-world). useSearchParams() returns the current URL search params (e.g., ?page=2). Both are client-side hooks — they only work in Client Components. They're useful for: active link highlighting, conditional rendering based on route, and reading query parameters.

useSearchParams() returns a URLSearchParams object. You can call .get('page'), .has('sort'), or .toString() on it. It's read-only — to update search params, use useRouter().push() or Link with the new URL.

There's also useParams() which returns the route params (the [slug] values). Unlike params in page components (which is a Promise), useParams() returns a synchronous object. It works in both Server and Client Components.

app/components/NavLink.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
'use client'

import { usePathname } from 'next/navigation'
import Link from 'next/link'

export default function NavLink({ href, children }: {
  href: string
  children: React.ReactNode
}) {
  const pathname = usePathname()
  const isActive = pathname === href || pathname.startsWith(href + '/')
  
  return (
    <Link
      href={href}
      className={isActive ? 'text-blue-500 font-bold' : 'text-gray-500'}
    >
      {children}
    </Link>
  )
}
Try it live
usePathname for active links
Compare usePathname() with the link href to highlight the active navigation item.
Production Insight
A team used usePathname() to highlight the active navigation item. But they compared the full pathname, so /blog/hello-world didn't match /blog. The fix: use pathname.startsWith('/blog') to match all blog routes, or use a more sophisticated matching function.
Key Takeaway
- usePathname() returns the current path — use it for active link highlighting
- useSearchParams() returns query parameters — use it for pagination, filters, sorting
- useParams() returns route params — works in both Server and Client Components
- All three hooks are read-only — use useRouter().push() to change the URL

The 404 Page — Custom Not-Found Routes

You can create a custom 404 page by adding app/not-found.tsx. This file exports a component that renders when no route matches. It's scoped — if you put app/blog/not-found.tsx, it only shows for 404s under /blog/*. The root app/not-found.tsx catches all unmatched routes.

But there's a subtlety: not-found.tsx only shows for routes that don't exist. If a route exists but the data isn't found (e.g., a blog post with a non-existent slug), you need to call notFound() from next/navigation in your page component. This triggers the nearest not-found.tsx boundary.

notFound() is a function you call from Server Components. It throws a NOT_FOUND error that Next.js catches and renders the nearest not-found.tsx. This is different from returning a 404 response — it uses the layout hierarchy to show the right error UI.

app/not-found.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Link from 'next/link'

export default function NotFound() {
  return (
    <div className="flex flex-col items-center justify-center min-h-[60vh]">
      <h1 className="text-4xl font-bold">404</h1>
      <p className="text-gray-500 mt-2">Page not found</p>
      <Link
        href="/"
        className="mt-4 px-4 py-2 bg-blue-500 text-white rounded"
      >
        Go home
      </Link>
    </div>
  )
}
Try it live
notFound() for data-level 404s
Use notFound() when data is missing (e.g., blog post doesn't exist). The route exists but the data doesn't.
Production Insight
A team's blog returned a generic 500 error when a post was deleted. The error page didn't match the site design. They added notFound() in the blog post page and created app/blog/not-found.tsx with a custom design. Users now see a branded "Post not found" page instead of a generic error.
Key Takeaway
- Create app/not-found.tsx for a custom 404 page
- Call notFound() from Server Components when data is missing
- Scoped not-found.tsx in a route segment only catches 404s in that segment
- The 404 page inherits the layout hierarchy — it's wrapped in parent layouts

The Migration Path — From Pages Router to App Router

Migrating from Pages Router to App Router is not a one-day task. It's a gradual process where you move routes one at a time. Both routers can coexist — App Router routes take priority for matching paths. You can have pages/about.js and app/about/page.tsx — the App Router version wins.

The migration steps: 1) Create app/layout.js with the root layout (move content from pages/_app.js and pages/_document.js), 2) Move routes one at a time from pages/ to app/, 3) Replace getServerSideProps with async Server Components, 4) Replace getStaticProps with fetch() in Server Components, 5) Replace getStaticPaths with generateStaticParams.

The hardest part is migrating data fetching. getServerSideProps becomes an async Server Component. getStaticProps becomes fetch() with next: { revalidate }. getStaticPaths becomes generateStaticParams. The patterns are similar but the syntax is different.

app/blog/[slug]/page.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { notFound } from 'next/navigation'

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await getPost(slug)
  
  // If the post doesn't exist, show the 404 page
  if (!post) {
    notFound()
  }
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  )
}
Try it live
Migrate routes one at a time
Both routers can coexist. Move one route from pages/ to app/, test it, then move the next.
Production Insight
A team migrated 200 routes from Pages Router to App Router over 3 months. They used a shared layout in app/layout.js and moved routes one at a time. Each migration was a separate PR with before/after performance comparisons. The result: 40% smaller bundles, 60% faster navigation, and zero downtime.
Key Takeaway
- Both routers can coexist — migrate one route at a time
- App Router routes take priority over Pages Router routes for the same path
- getServerSideProps → async Server Component, getStaticPropsfetch() with revalidate
- getStaticPathsgenerateStaticParams, _app.jsapp/layout.js
● Production incidentPOST-MORTEMseverity: high

Missing layout.js — 47 Pages Returned 404 in Production

Symptom
All routes under /dashboard/* returned 404. The root / and /login worked fine. Error rate spiked from 0.1% to 100% on dashboard routes.
Assumption
The team assumed layout.js was optional — they thought it was like a React wrapper component that could be omitted.
Root cause
A developer deleted app/(dashboard)/layout.js thinking it was unused. Next.js requires a layout chain from root to every leaf page. Without layout.js in the dashboard route group, all child routes (/dashboard/settings, /dashboard/analytics, etc.) returned 404 because Next.js couldn't find a valid layout to render them in.
Fix
Restored the deleted layout.js file. Added a CI lint rule that checks for missing layout files in route segments. Added a pre-push hook that runs next build to catch missing layouts before deployment.
Key lesson
  • Every route segment needs a layout.js or page.js — layouts don't replace pages
  • The root layout.js is required — deleting it breaks every route in the app
  • Add a CI step that runs next build to catch missing layouts before deployment
  • Use route groups (group) to organize routes without affecting the URL structure
Production debug guideHow to diagnose 404s, wrong layouts, and missing routes4 entries
Symptom · 01
Route returns 404
Fix
Check if every segment in the path has a page.js file. Check if a parent layout.js is missing. Check for case sensitivity issues.
Symptom · 02
Wrong layout is wrapping a page
Fix
Check the folder hierarchy — layouts wrap all child routes. Use route groups (group) to organize layouts without affecting URLs.
Symptom · 03
Dynamic route [slug] returns 404
Fix
Check if generateStaticParams is missing for static export. Check if the dynamic segment name matches between folder and component.
Symptom · 04
Layout re-renders on every navigation
Fix
You might be using template.js instead of layout.js. Templates re-render on every navigation; layouts persist. Rename to layout.js if you want persistent state.
★ Quick Debug ReferenceFast commands for diagnosing routing issues
Route returns 404
Immediate action
Check if page.js exists in the route segment
Commands
ls -la app/**/page.{tsx,jsx,js} 2>/dev/null
find app -name 'page.*' -type f
Fix now
Create app/<segment>/page.tsx with a default export component
Wrong layout wrapping a page+
Immediate action
Check the folder hierarchy for layout.js files
Commands
find app -name 'layout.*' -type f
ls -la app/**/layout.*
Fix now
Move the layout.js to the correct parent folder or use route groups (group)
Dynamic route [slug] returns 404+
Immediate action
Check if generateStaticParams is needed
Commands
ls -la app/**/\[*\]*/
grep -r 'generateStaticParams' app/
Fix now
Add generateStaticParams export or ensure the route is not static-exported
⚙ Quick Reference
17 commands from this guide
FileCommand / CodePurpose
applayout.tsxexport default function RootLayout({The Filesystem IS the Router
appbloglayout.tsxexport default function BlogLayout({Layouts
appblogtemplate.tsx'use client'Templates
app(dashboard)layout.tsxexport default function DashboardLayout({Route Groups
appblog[[...slug]]page.tsxexport default async function BlogPage({Dynamic Routes
app@feedpage.tsxexport default async function Feed() {Parallel Routes
appfeed(..)photo[id]page.tsxexport default async function PhotoModal({Intercepting Routes
appblogloading.tsxexport default function BlogLoading() {Loading and Error States
appblog[slug]page.tsxexport async function generateMetadata({Metadata
apperror.tsx'use client'The 404 Bug
appcomponentsNavigation.tsxexport default function Navigation() {The Link Component
appcomponentsLoginForm.tsx'use client'The useRouter Hook
applayout.tsxexport const metadata: Metadata = {The Root Layout
appcomponentsPagination.tsxexport default function Pagination({ page, totalPages }: {The Link Component
appcomponentsNavLink.tsx'use client'The usePathname and useSearchParams Hooks
appnot-found.tsxexport default function NotFound() {The 404 Page
appblog[slug]page.tsxexport default async function BlogPost({The Migration Path

Key takeaways

1
The filesystem IS the router
app/blog/[slug]/page.tsx = /blog/:slug, no config needed
2
Every route segment needs a page.js
a layout.js alone is not enough
3
Missing layout.js in any parent segment causes 404 on all child routes
4
Layouts persist across navigations; templates re-render on every navigation
5
Route groups (group) organize routes without affecting URLs
6
Parallel routes @slot render independent UIs in the same layout
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the difference between layout.js and template.js in the App Rout...
Q02SENIOR
How do you handle 404s for dynamic routes like blog posts?
Q03SENIOR
What are parallel routes and when would you use them?
Q04SENIOR
How does Next.js decide which layout to use for a given route?
Q01 of 04SENIOR

Explain the difference between layout.js and template.js in the App Router.

ANSWER
Both wrap child pages, but they differ in lifecycle. Layouts persist across navigations — when the user navigates from /blog to /blog/post-1, the layout does NOT re-render. Only the page.tsx content changes. This means layout state (scroll position, form inputs, sidebar state) is preserved. Templates re-render on every navigation — React unmounts and remounts the component tree. Use layouts for persistent UI (headers, footers, sidebars). Use templates for animations, analytics page views, or any UI that should reset on navigation.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Why does my route return 404 when the file exists?
02
What's the difference between layout.js and template.js?
03
Can I use both App Router and Pages Router in the same project?
04
How do I create a catch-all route in the App Router?
05
Can I use React Router with Next.js?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.

Follow
Verified
production tested
July 12, 2026
last updated
1,787
articles · all by Naren
🔥

That's Next.js. Mark it forged?

11 min read · try the examples if you haven't

Previous
Getting Started with Next.js 16: Installation and Project Structure
21 / 56 · Next.js
Next
Server Components vs Client Components in Next.js 16