Pages and Routing
File-based routing with the App Router, nested layouts, dynamic segments, and linking with next/link.
File-based routing, in practice
Each segment of a URL corresponds to a folder in app/, and a page.tsx file inside a folder is what makes that segment a navigable page rather than just an organizational folder:
// app/page.tsx → the "/" route
export default function HomePage() {
return <h1>Welcome home</h1>;
}
// app/about/page.tsx → the "/about" route
export default function AboutPage() {
return <h1>About us</h1>;
}
Layouts
A layout.tsx file wraps every page (and every nested layout) beneath it in the folder tree, and — critically — persists across navigations between sibling pages instead of re-rendering from scratch:
// app/layout.tsx — the root layout, required, wraps the entire app
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<nav>My Site</nav>
{children}
</body>
</html>
);
}
// app/blog/layout.tsx — wraps every page under /blog only
export default function BlogLayout({ children }: { children: React.ReactNode }) {
return (
<div className="blog-layout">
<aside>Blog sidebar</aside>
<main>{children}</main>
</div>
);
}
Layouts nest: a page at app/blog/[slug]/page.tsx renders inside BlogLayout, which itself renders inside RootLayout.
Dynamic segments
Wrapping a folder name in square brackets creates a dynamic segment that matches any value at that position in the URL:
// app/blog/[slug]/page.tsx
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>Post: {slug}</h1>;
}
/blog/hello-world renders this page with slug equal to "hello-world". As of Next.js 15, params (and searchParams) are provided as a Promise that must be awaited — this changed from earlier versions, where params was a plain object, to support Next's async request APIs consistently.
Two further variants exist for matching more than one segment:
app/shop/[...slug]/page.tsx— a catch-all segment, matching/shop/a,/shop/a/b,/shop/a/b/c, withslugas an array.app/shop/[[...slug]]/page.tsx— an optional catch-all, which additionally matches/shopitself (empty array), not just deeper paths.
Linking between pages
Use next/link instead of a plain <a> tag — it enables client-side navigation (no full page reload) and automatically prefetches the linked page's code when it scrolls into view:
import Link from 'next/link';
export default function Nav() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog/hello-world">A specific post</Link>
</nav>
);
}
Common mistakes
- Using a plain
<a href="/about">instead of<Link>— it still works, but forces a full page reload and loses Next's prefetching, defeating much of the point of client-side routing. - Forgetting to
await paramsin Next.js 15+ — treating it as a plain object throws a runtime error, since it's now aPromise. - Naming a folder
[slug]when the route actually needs to match multiple segments — that requires the[...slug](or[[...slug]]) catch-all syntax instead.