Pages Router
The Pages Router is the original Next.js router: every file in pages/
is a route, and [param] in a file name makes a dynamic route. All
components are client components that are prerendered on the server and
hydrated; data comes from getStaticProps / getServerSideProps.
App-wide wrappers live in _app and _document. It is legacy:
supported, but new projects use the App Router.
Applies to: Next.js 16.4 (Pages Router).
Details
Routes
| File | URL | router.query |
|---|---|---|
pages/index.js | / | |
pages/blog/first-post.js | /blog/first-post | |
pages/posts/[id].js | /posts/a | { id: 'a' } |
pages/shop/[...slug].js | /shop/a/b | { slug: ['a', 'b'] } |
pages/shop/[[...slug]].js | /shop and deeper | { slug: undefined } on /shop |
pages/api/users.js | /api/users | API Route, not a page |
A catch-all [...slug] does not match /shop; the optional one [[...slug]]
does.
Special Files
_app.jswraps every page: global CSS, providers, a shared layout. It receivesComponent(the current page) andpageProps(from the data fetching functions)._document.jscustomizes the<html>and<body>markup. It is rendered only on the server, so it has no event handlers or state.404.js,500.js: custom error pages.
Navigation
<Link href="/about">navigates on the client and prefetches the page in production when the link is in the viewport.useRouter()fromnext/router(notnext/navigation) givespush,replace,back,query,pathname,isFallback.withRouteris the HOC version for class components.
Redirects
| Where | How |
|---|---|
next.config redirects | { source, destination, permanent } for known URLs |
Proxy (proxy.ts) | NextResponse.redirect(new URL(...)), see Proxy |
getServerSideProps | Return { redirect: { destination, permanent } } |
getStaticProps | The same, but not during the build; works on ISR revalidation |
| Client | router.push() / router.replace() |
Example
pages/posts/[id].js
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Post() {
const router = useRouter();
// A static page gets the query only after hydration
if (!router.isReady) return null;
const id = Number(router.query.id);
return (
<>
<h1>Post {id}</h1>
<Link href="/posts">Back</Link>
<button onClick={() => router.push(`/posts/${id + 1}`)}>Next</button>
</>
);
}
Follow-up Questions
- Why is
router.queryempty on the first render of a static page? The page is prerendered without the query; it is filled after hydration (router.isReady). - How do you share a layout between pages? Render it in
_app, or use the per-pagegetLayoutpattern to keep the layout's state between pages. - Can you use Server Components in the Pages Router? No: all components in
pages/are prerendered and hydrated. The routers can be mixed per route, not inside one page.
Common Mistakes
- Importing
useRouterfromnext/routerinapp/(it throws), or expectingqueryandisReadyfrom thenext/navigationone inpages/.