# Zones on the Pages Router
A zone may use the App Router, the Pages Router, or both, like any Next.js app. A zone on the Pages Router is built,
installed, swapped and rolled back like any other zone: in mode `"zones"` (live installs), in mode `"single"`, with
Next's `output: "standalone"`, and in `next-zones dev`.
## Its files
```
docs/
next.config.mjs zoneConfig({ mount: "/docs" })
pages/
_app.tsx the zone's own (optional)
_document.tsx the zone's own (optional)
docs/
index.tsx /docs
[slug].tsx /docs/a, /docs/b…
public/docs/… served at /docs/…
```
- **Every page lives under the mount**: `pages/docs/…` (or `pages/docs.tsx` for `/docs` itself). A page anywhere else
(`pages/about.tsx`) is outside the mount and refused, as an `app/` route would be.
- **`_app`, `_document`, `_error`, `404` and `500`** sit at the top of `pages/`, as in any Next app. They are the zone's
own: its pages render with them, never with another zone's.
- **`pages/api/`** is served at `/api/…`, which no zone but the shell owns: a zone's API routes are refused there. Write
them as route handlers under the mount (`app/docs/api/…/route.ts`), which works in a Pages Router zone too (route
handlers need no root layout; a path both `app/` and `pages/` define is refused by `next build` itself), or put them
in the shell.
- **What every zone shares** (the navigation, the providers) comes in through its `_app`: a Pages Router page never
renders the shell's App Router layout. Import the same shared components there.
- **The client runtime check** Zones runs on a zone's App Router code (the shell's runtime must have what it uses)
does not apply to its Pages Router pages, which run on the zone's own runtime.
## How its pages are served
Next never shows a Pages Router page and an App Router page in one document: going from one to the other loads a new
document. So a Pages Router page renders whole in its own zone's build, as it would in that zone alone:
- **its own document**: the zone's `_document` and `_app`, its build id, its client runtime and chunks;
- **soft navigation inside the zone**: `` between the zone's Pages Router pages keeps its `_app`'s state, and
each page's data comes from `/_next/data//…`, which Zones answers;
- **between zones**: to the shell, to an App Router zone or to another Pages Router zone, the next page loads a new
document (Next's own rule between the two routers, and two builds' Pages Router clients never share one);
- **data**: `getStaticProps` (prerendered, `revalidate`, `notFound`), `getStaticPaths` (checked with
`fallback: "blocking"`), `getServerSideProps`, and static pages, as in Next. A prerendered page is served as built; a
revalidated one is written to the version's cache, never into the zone image;
- **not found**: an unknown URL under the mount, and a page answering `notFound`, show the shell's not-found page
(404), as for an App Router zone: the zone's own `404`, `500` and `_error` are used when it runs alone.
## Live installs
A new version of a Pages Router zone installs like any other. Its pages then render with the new build (its new build
id); requests for the old version's data are answered 404, and Next's client reloads the page from the server.
To make open tabs follow at once, render [``](updates.md) in the zone's `_app`:
```tsx
// docs/pages/_app.tsx
import type { AppProps } from "next/app";
import { ZoneUpdates } from "@runsnip/next-zones/client";
export default function App({ Component, pageProps }: AppProps) {
return (
<>
>
);
}
```
After a swap, the tab's next navigation (a link, `router.push`, back or forward) loads a new document, which is the
version just installed. Nothing reloads before the user navigates. Without it, a page the tab prefetched before the
swap may show once more from the old version.
The static files of every version still in the store stay served, so a tab still on a replaced version keeps loading
its chunks.
## In the other modes
- **`mode: "single"`**: the zone's pages are linked into the one app, each rendering from its own build under
`.next/zones//`; going to another zone loads a new document, as under Zones. The app's `instrumentation.js` (which Next loads before any route renders) gets a few lines that
make the Pages Router read a zone page's build id and build manifest from its zone's build.
- **`output: "standalone"`**: the packages a zone's server code leaves external (a Pages Router build leaves most of
`node_modules` external) are traced into the standalone folder, with what they import.
- **`next-zones dev`**: one `next dev` serves the shell and every zone. Each zone's pages are re-exported into the
composed app (a stub per page, so an edit to the page is seen at once, with HMR). With one `_app` (or `_document`)
in the workspace it is used as it is; with several, each page renders with its own zone's, picked by the page's
mount. Moving between two Pages Router zones is a soft navigation in dev (one app), a new document under Zones.
## Measured
Sequential requests after a warm-up (`tools/bench/latency.mjs`, 3000 per path, 300 warm-up), Node 24.16, Apple M1,
two alternating rounds (the two numbers), p50 in ms. The reference is the same pages in one app on `next start` with
the same shell, whose proxy runs on every request in both:
| Path | Zones | One app |
|---|---|---|
| `getServerSideProps` page | 0.98, 1.07 | 0.85, 0.99 |
| prerendered `getStaticProps` page | 0.69, 0.65 | 0.58, 0.60 |
| its `/_next/data/…` JSON | 0.44, 0.45 | 0.43, 0.39 |
Zones' own code is about 5% of the busy time in a CPU profile (`spikes/zones/RESULTS.md`, "The Pages Router").