Last updated: 2026-10-08

Pages and Layouts

Every visible part of a Next.js application is composed of two kinds of files working together: pages and layouts. Pages define what is unique to a route — the content that changes as a user navigates. Layouts define what persists — the structure that wraps that content and survives navigation without remounting. Together, they are how Next.js assembles a UI from the file system.

Understanding the difference between them is not just a matter of knowing which file to create. It is a matter of understanding how Next.js thinks about rendering, persistence, and composition.


Pages

A page is a React component exported from a page.tsx file. It is the unique UI for a route — what a user sees when they visit a specific URL. Without a page.tsx file, a folder in the app/ directory contributes to the URL hierarchy but is not publicly accessible. The page is what makes the route real.

// app/page.tsx  →  renders at /
export default function HomePage() {
  return <h1>Welcome</h1>;
}
// app/about/page.tsx  →  renders at /about
export default function AboutPage() {
  return <h1>About Us</h1>;
}

Pages are Server Components by default. This means they can be declared as async functions and fetch data directly — no useEffect, no API call from the browser, no separate loading state to manage manually.

// app/blog/page.tsx
export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

The fetch happens on the server during rendering. By the time the HTML reaches the browser, the posts are already in it. The client does not make a second request to retrieve the data.


Layouts

A layout is a React component exported from a layout.tsx file. It wraps the page at its level and all pages beneath it in the directory hierarchy. Unlike pages, layouts do not unmount when the user navigates between routes within their scope — they persist, re-rendering only if their own props change.

This persistence is one of the most practically significant behaviours in the App Router. A navigation bar, a sidebar, a header with authentication state — anything that should remain stable while the page content changes belongs in a layout.

The Root Layout

Every Next.js application must have a root layout at app/layout.tsx. It is the outermost wrapper for the entire application and the only place in the project where the <html> and <body> tags are defined.

// app/layout.tsx
import './globals.css';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

The children prop receives the current page — or the next layout in the hierarchy, if one exists. This is how the nesting works: each layout renders its children, which may themselves be layouts, until the innermost page.tsx is reached.

The root layout cannot be a Client Component. It runs on the server for every request. This means it can fetch data, access environment variables, and perform server-only operations — but it cannot use useState, useEffect, or any browser API directly. Any interactivity the root layout needs must be encapsulated in a Client Component imported into it.

Nested Layouts

Layouts nest. A layout.tsx file at app/dashboard/layout.tsx wraps every route under /dashboard, and it is itself wrapped by the root layout at app/layout.tsx. The full layout tree is composed at render time, from outermost to innermost.

app/
├── layout.tsx           ← root layout — wraps everything
├── page.tsx             → /
└── dashboard/
    ├── layout.tsx       ← dashboard layout — wraps /dashboard/*
    ├── page.tsx         → /dashboard
    └── settings/
        └── page.tsx     → /dashboard/settings

When a user visits /dashboard/settings, the rendered hierarchy is:

<RootLayout>
  <DashboardLayout>
    <SettingsPage />
  </DashboardLayout>
</RootLayout>

When they navigate to /dashboard, only SettingsPage is replaced by DashboardPage. The root layout and the dashboard layout remain mounted — they do not re-render, their state is preserved, and any side effects they have already triggered do not run again.

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="dashboard-shell">
      <nav>Dashboard Navigation</nav>
      <main>{children}</main>
    </div>
  );
}

This dashboard layout renders a navigation sidebar alongside whatever page is currently active beneath it. As the user moves between /dashboard, /dashboard/settings, and any other route under the dashboard, the sidebar stays exactly where it is.


Metadata

Both pages and layouts can export a metadata object or a generateMetadata function to define the document's <title>, description, Open Graph tags, and other head-level information. Next.js merges metadata from all layouts and the current page, with the page's metadata taking precedence over layout metadata where they conflict.

Static Metadata

// app/blog/page.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'The Blog',
  description: 'Thoughts and research from WinehouseLabs.',
};

export default function BlogPage() {
  return <h1>The Blog</h1>;
}

Dynamic Metadata

When the metadata depends on data — a blog post's title, a product's name — generateMetadata is used instead. It is an async function that receives the same route parameters as the page and returns a metadata object.

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';

export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}): Promise<Metadata> {
  const { slug } = await params;
  const post = await fetch(`https://api.example.com/posts/${slug}`).then(r => r.json());

  return {
    title: post.title,
    description: post.excerpt,
  };
}

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await fetch(`https://api.example.com/posts/${slug}`).then(r => r.json());

  return <article>{post.content}</article>;
}

Next.js deduplicates the fetch requests here — even though the same URL is fetched in both generateMetadata and the page component, the result is cached and the request is only made once.

Metadata Inheritance

Metadata is inherited and merged down the layout hierarchy. A title defined in the root layout applies to every page unless overridden. A page that defines its own title overrides the root layout's title for that route only.

Next.js supports a title.template pattern that makes this inheritance clean in practice:

// app/layout.tsx
export const metadata: Metadata = {
  title: {
    default: 'WinehouseLabs',
    template: '%s — WinehouseLabs',
  },
};

With this template, a page that sets its title to 'The Blog' produces the document title 'The Blog — WinehouseLabs'. A page that sets no title at all uses the default: 'WinehouseLabs'. The branding is consistent without having to repeat it in every page's metadata definition.


Templates

A template.tsx file is a close relative of layout.tsx, with one important behavioural difference: templates remount on every navigation, rather than persisting. Every time the user navigates to a route within the template's scope, the template component is unmounted and a fresh instance is mounted.

// app/dashboard/template.tsx
export default function DashboardTemplate({
  children,
}: {
  children: React.ReactNode;
}) {
  return <div>{children}</div>;
}

This distinction makes templates appropriate for a narrow set of use cases — animations that should play each time a page is entered, features that rely on useEffect firing on every navigation, or cases where shared UI state between pages is explicitly unwanted. For the vast majority of use cases, layout.tsx is the correct choice. Templates exist for when layout's persistence is the problem rather than the solution.


Not Found Pages

A not-found.tsx file defines what is rendered when a route returns a "not found" response. It can be placed at the root level to handle all 404 responses across the application, or within a specific route segment to handle not-found conditions for just that section.

// app/not-found.tsx
export default function NotFound() {
  return (
    <div>
      <h1>404 — Page Not Found</h1>
      <p>The page you are looking for does not exist.</p>
    </div>
  );
}

In a Server Component, the not-found state is triggered by calling notFound() from next/navigation:

import { notFound } from 'next/navigation';

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await fetch(`https://api.example.com/posts/${slug}`).then(r => r.json());

  if (!post) {
    notFound();
  }

  return <article>{post.content}</article>;
}

When notFound() is called, Next.js stops rendering the current page and renders the nearest not-found.tsx in the hierarchy instead. It is a clean, explicit way to handle missing resources without crashing the page or returning misleading content.


Summary

ConceptFileBehaviour
Pagepage.tsxUnique UI for a route — makes the route accessible
Root layoutapp/layout.tsxRequired — wraps every page in the application
Nested layoutapp/[segment]/layout.tsxWraps all pages under a route segment
Layout persistence—Layouts stay mounted across navigation within their scope
Static metadataexport const metadataFixed title, description, and head tags
Dynamic metadataexport async function generateMetadataData-driven title and head tags
Title templatetitle.template in root layoutConsistent branding appended to every page title
Templatetemplate.tsxLike a layout, but remounts on every navigation
Not foundnot-found.tsxUI shown when notFound() is called or a route does not exist