| | |

Next.js 03 ▲ Layouts and Pages — Root layouts, nested routes & Link component

⏱️ Estimated read: 14 min | |

The App Router is built on two special files: the page.tsx, which defines the UI unique to a route, and the layout.tsx, which defines the UI shared between multiple pages. The previous chapter described the folder structure; this chapter explains the files that make the structure functional. A folder becomes a publicly accessible route only when it contains a page.tsx, and the layout.tsx wraps the pages and the nested layouts without re-rendering on navigation.

The root layout is the single required layout, and it must contain the <html> and <body> tags . The nested layouts are optional, and each one wraps the routes in its segment and the segments below it. The Link component is the navigation primitive, and it extends the HTML <a> element with the client-side navigation and the prefetching, which is the mechanism that makes the route transitions instant .

Key point: The root layout is required and must return the <html> and <body> tags. Only the root layout can contain these tags; the nested layouts must not repeat them, or the browser receives the invalid nested HTML . The Link component is not a replacement for the <a> element; it renders an <a> element with the enhanced behavior. The href prop is the destination, and the prefetch prop controls whether the route is loaded in the background before the user clicks .


Why layouts and pages require the special-file conventions

The page problem.

A folder is not a route until it contains a page.tsx. The app/blog/page.tsx is the /blog route, and the app/blog/[slug]/page.tsx is the /blog/:slug dynamic route . The page is the leaf of the route subtree, and it is the UI that is unique to the route. The page is a Server Component by default, but it can be marked as a Client Component with the 'use client' directive.

The layout problem.

A layout is the UI that is shared between the pages. On navigation, the layout preserves its state, remains interactive, and does not re-render . This is the performance benefit: the layout is rendered once, and only the page inside it changes. The nested layouts are the mechanism for the different sections that have the different chrome, such as the marketing site and the dashboard.

The Link problem.

The <a> element triggers the full page reload, which discards the React state and the layout. The Link component intercepts the click and performs the client-side navigation, which preserves the layout and the state . The prefetching is the second benefit: the Link component prefetches the route when it enters the viewport, so the navigation is instant when the user clicks .


a. The root layout and the nested layouts

The root layout is defined in app/layout.tsx, and it must contain the <html> and <body> tags.

// app/layout.tsx (required)
import type { Metadata } from 'next'
import './globals.css'

export const metadata: Metadata = {
  title: 'My App',
  description: 'The Next.js application',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <header>My App</header>
        <main>{children}</main>
        <footer>© 2025</footer>
      </body>
    </html>
  )
}

The RootLayout receives the children prop, which is the page or the nested layout . The <html> and the <body> tags are the required structure, and the metadata export is the mechanism for the page title and the description . The global CSS is imported here, so it applies to every route.

The nested layout is defined in the subfolder, and it wraps the routes in its segment.

// app/blog/layout.tsx
export default function BlogLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <section>
      <nav>Blog Navigation</nav>
      {children}
    </section>
  )
}

The BlogLayout is rendered inside the RootLayout, and it receives the blog page or the blog post page as its children . The layout preserves its state across the navigations within the /blog segment, and the RootLayout preserves its state across all the navigations. The nested layout must not contain the <html> or the <body> tags, because the root layout already provides them .


b. The pages and the dynamic segments

A page is the UI unique to a route, and it is defined in the page.tsx file.

// app/page.tsx (the / route)
export default function HomePage() {
  return <h1>Welcome</h1>
}

// app/about/page.tsx (the /about route)
export default function AboutPage() {
  return <h1>About</h1>
}

// app/blog/page.tsx (the /blog route)
export default async function BlogPage() {
  const posts = await getPosts()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.slug}>{post.title}</li>
      ))}
    </ul>
  )
}

The HomePage is the / route, and the AboutPage is the /about route . The BlogPage is the /blog route, and it is an async function, which means it can await the data directly because it is a Server Component by default.

The dynamic segment is defined with the square brackets in the folder name.

// app/blog/[slug]/page.tsx (the /blog/:slug route)
export default async function BlogPostPage({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await getPost(slug)
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  )
}

The [slug] folder is the dynamic segment, and the params prop contains the value of the slug . The params is a promise in the recent Next.js versions, and it must be awaited before the values are used. The page is rendered for every value of the slug, which is the mechanism for the blog posts, the product pages, and the other data-driven routes.


c. The Link component and the navigation

The Link component is imported from next/link, and it accepts the href prop.

import Link from 'next/link'

export default function Navigation() {
  return (
    <nav>
      <Link href="/">Home</Link>
      <Link href="/about">About</Link>
      <Link href="/blog">Blog</Link>
      <Link href="/blog/hello-world">Hello World</Link>
    </nav>
  )
}

The Link renders an <a> element with the href, and it intercepts the click to perform the client-side navigation . The prefetch prop is the default null, which prefetches the static routes in the production build . The replace prop replaces the current history entry instead of pushing a new one, which is useful for the redirects and the login pages.

The dynamic links are created with the template literals.

import Link from 'next/link'

export default function PostList({ posts }: { posts: Post[] }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/blog/${post.slug}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  )
}

The href={/blog/${post.slug}} is the template literal that generates the URL for each post . The Link component accepts the string and renders the <a> element, and the navigation is the client-side transition.

The active link is highlighted with the usePathname hook.

'use client'

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

export function NavLinks() {
  const pathname = usePathname()

  return (
    <nav>
      <Link className={pathname === '/' ? 'active' : ''} href="/">
        Home
      </Link>
      <Link className={pathname === '/about' ? 'active' : ''} href="/about">
        About
      </Link>
    </nav>
  )
}

The usePathname is a client hook, so the component must be a Client Component ('use client') . The pathname is compared with the href to add the active class to the current link.


Complete Example Session

// ============================================
// PART 1: THE ROOT LAYOUT
// ============================================
// app/layout.tsx
import type { Metadata } from 'next'
import './globals.css'

export const metadata: Metadata = {
  title: 'My App',
  description: 'The Next.js application',
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <header>My App</header>
        <main>{children}</main>
      </body>
    </html>
  )
}

// ============================================
// PART 2: THE HOME PAGE
// ============================================
// app/page.tsx
export default function HomePage() {
  return <h1>Welcome</h1>
}

// ============================================
// PART 3: THE NESTED LAYOUT
// ============================================
// app/blog/layout.tsx
export default function BlogLayout({ children }: { children: React.ReactNode }) {
  return (
    <section>
      <nav>Blog Navigation</nav>
      {children}
    </section>
  )
}

// ============================================
// PART 4: THE NESTED PAGE
// ============================================
// app/blog/page.tsx
export default function BlogPage() {
  return <h1>Blog Posts</h1>
}

// ============================================
// PART 5: THE DYNAMIC PAGE
// ============================================
// app/blog/[slug]/page.tsx
export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  return <h1>Post: {slug}</h1>
}

// ============================================
// PART 6: THE LINK NAVIGATION
// ============================================
import Link from 'next/link'

export default function Navigation() {
  return (
    <nav>
      <Link href="/">Home</Link>
      <Link href="/blog">Blog</Link>
    </nav>
  )
}

// ============================================
// PART 7: THE DYNAMIC LINKS
// ============================================
export function PostList({ posts }: { posts: { id: number; slug: string; title: string }[] }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/blog/${post.slug}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  )
}

// ============================================
// PART 8: THE ACTIVE LINK
// ============================================
'use client'
import { usePathname } from 'next/navigation'

export function ActiveLink({ href, children }: { href: string; children: React.ReactNode }) {
  const pathname = usePathname()
  return <Link className={pathname === href ? 'active' : ''} href={href}>{children}</Link>
}

The eight parts demonstrate the layouts and pages: the root layout, the home page, the nested layout, the nested page, the dynamic page, the link navigation, the dynamic links, and the active link. The critical structure is Part 1: the <html> and <body> tags are the root layout’s requirement, and the nested layout in Part 3 does not repeat them.


Quick Reference

Special Files

FilePurposeRequired
layout.tsxShared UI, wraps childrenRoot required
page.tsxRoute UI, makes route publicYes per route
loading.tsxSuspense fallbackNo
error.tsxError boundary ('use client')No
not-found.tsx404 UINo

Root Layout Rules

RuleDetail
Locationapp/layout.tsx
Required tags<html> and <body>
Nested layoutsMust NOT repeat the tags
MetadataUse the metadata export, not the <head> tags

Link Props

PropPurpose
hrefThe destination URL
replaceReplace the history entry
scrollControl the scroll behavior
prefetchControl the prefetching
onNavigateThe client-side navigation callback

Route Patterns

PathURL
app/page.tsx/
app/about/page.tsx/about
app/blog/[slug]/page.tsx/blog/:slug
app/blog/layout.tsxWraps /blog/*

Best Practices

✅ Do This:

// Put the html and body in the root layout only
<html><body>{children}</body></html>                          // ✅

// Use the metadata export for the page title
export const metadata: Metadata = { title: 'My App' }          // ✅

// Use the Link component for the internal navigation
<Link href="/about">About</Link>                               // ✅

// Await the params in the dynamic page
const { slug } = await params                                  // ✅

// Use the usePathname for the active link
const pathname = usePathname()                                 // ✅

❌ Don’t Do This:

// Repeat the html and body in the nested layout
<html><body>{children}</body></html>    // Invalid nested HTML  // ❌

// Use the <a> element for the internal navigation
<a href="/about">About</a>    // Full page reload               // ❌

// Add the <title> tag in the root layout
<title>My App</title>    // Use the metadata export instead     // ❌

// Forget the await on the params
function Page({ params }) { const { slug } = params }    // Error # ❌

// Use the usePathname in a Server Component
'use client'    // The hook requires the client                 // ✅ (do this)

Common Pitfalls

PitfallWhy It HappensFix
The build error about the root layoutMissing <html> or <body>Add the tags
The nested HTML warningThe nested layout repeats the tagsRemove them from the nested layout
The navigation reloads the pageUsed <a> instead of LinkUse the Link component
The params are a promiseThe recent Next.js versionAwait the params
The active link does not workUsed the hook in a Server ComponentAdd 'use client'

Real-World Examples

1. Root layout

export default function RootLayout({ children }) {
  return <html><body>{children}</body></html>
}

2. Nested layout

export default function BlogLayout({ children }) {
  return <section>{children}</section>
}

3. Home page

export default function Home() { return <h1>Home</h1> }

4. Dynamic page

export default async function Post({ params }) {
  const { slug } = await params
  return <h1>{slug}</h1>
}

5. Basic link

<Link href="/about">About</Link>

6. Dynamic link

<Link href={`/blog/${post.slug}`}>{post.title}</Link>

7. Replace history

<Link href="/login" replace>Login</Link>

8. Disable scroll

<Link href="/about" scroll={false}>About</Link>

9. Active link

'use client'
const pathname = usePathname()
<Link className={pathname === '/' ? 'active' : ''} href="/">Home</Link>

10. Metadata

export const metadata: Metadata = { title: 'My App' }

Visual

The Layout Nesting

┌──────────────────────────────────────────────────────────────┐
│  THE LAYOUTS WRAP THE PAGES AND THE NESTED LAYOUTS           │
│                                                              │
│  app/layout.tsx (root)                                       │
│  └── <html><body>                                            │
│      └── app/blog/layout.tsx                                 │
│          └── <section>                                       │
│              └── app/blog/page.tsx                           │
│                  └── <h1>Blog Posts</h1>                     │
│                                                              │
│  The root layout wraps the blog layout, which wraps the page.│
│  Only the root has the html and body tags.                   │
└──────────────────────────────────────────────────────────────┘

The Link Navigation

┌──────────────────────────────────────────────────────────────┐
│  THE Link COMPONENT PERFORMS THE CLIENT-SIDE NAVIGATION      │
│                                                              │
│  <a href="/about">      → full page reload                   │
│  → the layout and the state are lost                         │
│                                                              │
│  <Link href="/about">   → client-side navigation             │
│  → the layout and the state are preserved                    │
│  → the route is prefetched in the background                 │
└──────────────────────────────────────────────────────────────┘

The Dynamic Route

┌──────────────────────────────────────────────────────────────┐
│  THE [slug] FOLDER IS THE DYNAMIC SEGMENT                    │
│                                                              │
│  app/blog/[slug]/page.tsx                                    │
│       │                                                      │
│       ▼                                                      │
│  /blog/hello-world  → params.slug = "hello-world"            │
│  /blog/nextjs-guide → params.slug = "nextjs-guide"           │
│                                                              │
│  The page is rendered for every value of the slug.           │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Root layoutapp/layout.tsx, required
Root layout tags<html> and <body>
Nested layoutWraps the routes in its segment
Pagepage.tsx, makes the route public
Dynamic route[slug] folder
ParamsA promise in the recent versions
Linknext/link, client-side navigation
Active linkusePathname in a Client Component
MetadataThe metadata export, not the <head>
PrefetchThe Link default in the production

Key takeaways:

  • The root layout is required and must contain the <html> and <body> tags. The nested layouts must not repeat these tags, or the browser receives the invalid nested HTML. The root layout is the only place where the tags belong .
  • The page is the file that makes the route public. A folder without a page.tsx is not a route. The page is the leaf of the route subtree, and it is the UI unique to the route .
  • The nested layout wraps the routes in its segment. The app/blog/layout.tsx wraps the /blog and the /blog/:slug routes, and it preserves its state across the navigations within the segment .
  • The Link component is the navigation primitive. It renders an <a> element with the client-side navigation and the prefetching. The <a> element triggers the full page reload and loses the layout and the state .
  • The params is a promise in the recent versions. The dynamic page must await the params before the values are used. The [slug] folder is the dynamic segment, and the params.slug is the value .
  • The metadata export replaces the <head> tags. The root layout must not contain the manual <title> or <meta> tags. The metadata object is the mechanism for the page title and the description .

Remember: The Next.js layouts and pages are the file-system routing. The page.tsx makes the route public, and the layout.tsx wraps the pages and the nested layouts. The root layout is required and must contain the <html> and <body> tags; the nested layouts must not repeat them. The Link component is the navigation primitive that performs the client-side transition and the prefetching. The dynamic segments are the [slug] folders, and the params is a promise. The result is the file system that is the router, where the folders are the URLs, the pages are the content, and the layouts are the shared chrome.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!