Data fetching in the Pages Router
Read and write getStaticProps, getStaticPaths and getServerSideProps, ISR and SWR in a Pages Router app, and know what each becomes in the App Router.
- 01Load a page's data the Pages Router wayPick getStaticProps, getStaticPaths or getServerSideProps for a page, and read what the build printed and what each visit got.
- 02Keep static pages freshAdd revalidate for ISR, refresh a page from a signed webhook with res.revalidate, and fetch what changes by the minute with SWR.
- 03Map each one to the App RouterSay what replaces each function in an App Router app with Cache Components, and why a page with getServerSideProps can't stream.
A home page made during the build
Pages Router basics built the tea shop with a pages folder, Next.js's original router, which Next.js 16.4.3 still runs. Its tea page read the tea's id from the router in the browser, so its first HTML said Loading…. A Pages Router page has no Server Components, so its component can't wait for data while it renders on the server. Instead, the page file exports a function that loads the data first, and Next.js calls it. Here is the shop with its teas in data/teas.json, a stand-in for a database. It was built with next build, run with next start, and recorded:
pages/index.tsximport Head from "next/head";import Link from "next/link";import { getTeas, type Tea } from "@/lib/teas"; export async function getStaticProps() { const teas = await getTeas(); return { props: { teas } };} export default function Home({ teas }: { teas: Tea[] }) { return ( <> <Head> <title>Tea shop</title> </Head> <h1>Tea shop</h1> <ul> {teas.map((tea) => ( <li key={tea.id}> <Link href={`/teas/${tea.id}`}>{tea.name}</Link> </li> ))} </ul> </> );}$ next build▲ Next.js 16.4.3 (Turbopack) Running TypeScript ... Finished TypeScript ... Creating an optimized production build ...✓ Compiled successfully Collecting page data ...✓ Generating static pages (6/6) Finalizing page optimization ... Route (pages)┌ ● /├ /_app├ ○ /404└ /teas/[id] ├ ● /teas/green ├ ● /teas/mint └ ● /teas/lemon ○ (Static) prerendered as static content● (SSG) prerendered as static HTML (uses getStaticProps) # What .next/server/pages holds: 52 files, 85.8 kB (deeper folders added up).next/server/pages/404.html 2.8 kB.next/server/pages/500.html 2.7 kB.next/server/pages/_app.js 232 bytes.next/server/pages/_app.js.map 53 bytes.next/server/pages/_app.js.nft.json 12.8 kB.next/server/pages/_app/build-manifest.json 759 bytes.next/server/pages/_app/client-build-manifest.json 57 bytes.next/server/pages/_app/next-font-manifest.json 94 bytes.next/server/pages/_app/pages-manifest.json 30 bytes.next/server/pages/_app/react-loadable-manifest.json 2 bytes.next/server/pages/_document.js 256 bytes.next/server/pages/_document.js.map 53 bytes.next/server/pages/_document.js.nft.json 12.8 kB.next/server/pages/_document/next-font-manifest.json 94 bytes.next/server/pages/_document/pages-manifest.json 40 bytes.next/server/pages/_document/react-loadable-manifest.json 2 bytes.next/server/pages/_error.js 433 bytes.next/server/pages/_error.js.map 53 bytes.next/server/pages/_error.js.nft.json 13.2 kB.next/server/pages/_error/build-manifest.json 468 bytes.next/server/pages/_error/client-build-manifest.json 59 bytes.next/server/pages/_error/next-font-manifest.json 94 bytes.next/server/pages/_error/pages-manifest.json 34 bytes.next/server/pages/_error/react-loadable-manifest.json 2 bytes.next/server/pages/index.html 2.2 kB.next/server/pages/index.js 522 bytes.next/server/pages/index.js.map 53 bytes.next/server/pages/index.js.nft.json 13.3 kB.next/server/pages/index.json 169 bytes.next/server/pages/index.meta 178 bytes.next/server/pages/index/build-manifest.json 457 bytes.next/server/pages/index/client-build-manifest.json 53 bytes.next/server/pages/index/next-font-manifest.json 94 bytes.next/server/pages/index/pages-manifest.json 27 bytes.next/server/pages/index/react-loadable-manifest.json 2 bytes.next/server/pages/teas/[id].js 504 bytes.next/server/pages/teas/[id].js.map 53 bytes.next/server/pages/teas/[id].js.nft.json 13.7 kB.next/server/pages/teas/[id]/ 673 bytes, 5 files.next/server/pages/teas/green.html 2.0 kB.next/server/pages/teas/green.json 80 bytes.next/server/pages/teas/green.meta 192 bytes.next/server/pages/teas/lemon.html 2.0 kB.next/server/pages/teas/lemon.json 80 bytes.next/server/pages/teas/lemon.meta 192 bytes.next/server/pages/teas/mint.html 2.0 kB.next/server/pages/teas/mint.json 78 bytes.next/server/pages/teas/mint.meta 191 bytesLines 5 to 8 of pages/index.tsx export getStaticProps. Line 6 loads the teas with getTeas from lib/teas.ts, which reads data/teas.json. Line 7 returns an object with props, and Next.js gives those props to the page's component: Home gets teas on line 10. Lines 18 to 22 list each tea with a link to its page. The other files are the shop from Pages Router basics: pages/_app.tsx wraps every page in components/layout.tsx, with the Teas link and the search box, and next.config.ts and tsconfig.json are what create-next-app writes for a Pages Router project, without Cache Components.
The Terminal tab shows what the build decided. ● / is new: the legend says (SSG) prerendered as static HTML (uses getStaticProps). SSG stands for static site generation: the page's HTML was made once, during the build, with the data getStaticProps returned. ○ /404 is a page with no data function, prerendered as static content. /_app is listed without a symbol, because it isn't a page of its own. The tea pages, /teas/[id], come with three ● lines of their own; the section on getStaticPaths explains them.
getStaticProps ran once, during next build, and the build wrote two files for /. A visit to the address gets index.html. A click on a link to it gets index.json, and React renders the page in the browser with those props. Each tea page gets the same two files.A bakery bakes most bread before it opens, so the first customer gets a loaf at once. A birthday cake with a name on it is baked only when someone orders it. When a customer asks for bread after the morning batch is a day old, the baker hands over a loaf from that batch and puts a fresh one in the oven for the next customer. The board on the wall that says what's left changes during the day, without baking anything.
- In real life: Bread baked before the shop opens
- In Next.js:
getStaticProps: a page made duringnext build - In real life: A cake baked when a customer orders it
- In Next.js:
getServerSideProps: a page made for each request - In real life: A loaf baked again when someone asks after it's a day old
- In Next.js:
revalidate: a stored page made again after a request - In real life: The stock board the cashier updates while you wait
- In Next.js: SWR: a number fetched in the browser
A Pages Router page loads its data in a function it exports, and when that function runs decides how the page is made. getStaticProps runs during the build (and again for ISR), getServerSideProps runs for each request, and getStaticPaths lists which dynamic pages the build makes. Whatever the function returns as props goes to the page's component, on the server and in the browser.
getStaticProps: an HTML file and a JSON file
Open the Terminal tab again and scroll down. After the build, the recorder listed what .next/server/pages held. Most of it is the server's code and manifests. Three files belong to the home page:
index.html: the page's HTML, with the three teas in it. A visit to/gets this file.index.json: the propsgetStaticPropsreturned, as JSON.index.meta: a few details Next.js keeps about the stored page.
Each tea page got the same three, like teas/mint.html and teas/mint.json. Those names are what this build wrote, internals of Next.js 16.4.3, not something to build on. Now open the Network tab. The first response, /, has x-nextjs-cache: HIT: Next.js sent the stored page. Its Cache-Control: s-maxage=31536000 lets a shared cache, like a CDN, keep it for a year. Pick the second response, the page's data. Its address is /_next/data/<buildId>/index.json. The build id is a name next build makes up for each build, so the recording shows it as <buildId>. The body is the JSON file: {"pageProps":{"teas":[…]},"__N_SSG":true}. pageProps are the props, and __N_SSG marks them as getStaticProps data.
Props are public; the function is not
Every component of a Pages Router page also runs in the browser, so React needs the same props there to hydrate the page. The docs say any props can be seen in the initial HTML for this reason: they travel in the <script id="__NEXT_DATA__"> tag near the end of the page (the Network tab leaves out every script's text). So getStaticProps must never return a secret as a prop. The function itself is different: its code stays on the server. The recorder searched the JavaScript files / loads. They contain Tea shop, from the component, and not data/teas.json, from the function's import. Next.js removes getStaticProps and what only it imports from the browser's code.
A click loads the JSON, not the HTML
The browser only asks for a page's HTML when it loads the page as a whole. A Link to it works differently. The recorder opened / in real Chrome and clicked Mint tea. Open the Steps tab of the example above: the first step, the home page, already asked for the JSON of all three teas, because their links were on the screen. The click asked for /_next/data/<buildId>/teas/mint.json?id=mint again, and loaded no HTML page at all. Step through it below.
Home, then a click on Mint teaafter step 1A blank page
React isn't running yet: clicks do nothing.
The page isn't complete yet.
The browser asks the server for /.
The first requests for the teas' JSON are prefetches: the router loads what a visible link needs before the click. The second request for mint.json came when the pointer moved onto the link, so the page shows the newest copy. After the click, the step also lists a HEAD request for the same file, which asks for headers and no body. Next.js's router sends it in the background after a navigation to a getStaticProps page, so the server can make the page again if it's out of date (lines 2361 to 2376 of router.ts).
A few rules come with the function:
- Only a page can export it, not
_app,_documentor a component. - It can't read the request: no cookies, no headers. It runs before any visitor exists.
- Besides
props, it can returnnotFound: truefor a404, aredirect, andrevalidate, which the ISR section uses.
getStaticPaths, and teas added after the build
Each tea has its page, pages/teas/[id].tsx. The brackets make id a dynamic part of the address, so the one file answers /teas/green, /teas/mint and any other id. For the build to make these pages, it needs the list of ids. That list comes from getStaticPaths:
pages/teas/[id].tsximport Head from "next/head";import { useRouter } from "next/router";import { getTea, getTeas, type Tea } from "@/lib/teas"; export async function getStaticPaths() { const teas = await getTeas(); return { paths: teas.map((tea) => ({ params: { id: tea.id } })), fallback: false, };} export async function getStaticProps({ params }: { params: { id: string } }) { const tea = await getTea(params.id); if (!tea) return { notFound: true }; return { props: { tea } };} export default function TeaPage({ tea }: { tea: Tea }) { const router = useRouter(); if (router.isFallback) return <p>Loading the tea…</p>; return ( <> <Head> <title>{`${tea.name} | Tea shop`}</title> </Head> <h1>{tea.name}</h1> <p>{`$${tea.price} a box`}</p> </> );}GET /teas/mint200
- Response headers
x-nextjs-cacheHITCache-Controls-maxage=31536000X-Powered-ByNext.jsContent-Typetext/html; charset=utf-8VaryAccept-Encoding
The HTML that arrived, one element per line, with the code of its 11 scripts left out
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" data-next-head="">
<meta name="viewport" content="width=device-width" data-next-head="">
<title data-next-head="">Mint tea | Tea shop</title>
<link rel="preload" href="/_next/static/chunks/3lqpx571i07yu.css" as="style">
<link rel="stylesheet" href="/_next/static/chunks/3lqpx571i07yu.css" data-n-g="">
<noscript data-n-css=""></noscript>
<script>…</script>
<script src="/_next/static/chunks/3t_68x1ty_491.js" defer=""></script>
<script src="/_next/static/chunks/1_2oau8-1i67e.js" defer=""></script>
<script src="/_next/static/chunks/43cz2ihhg0zvk.js" defer=""></script>
<script src="/_next/static/chunks/10_fcp04d3__p.js" defer=""></script>
<script src="/_next/static/chunks/turbopack-3-ry682tkyiqk.js" defer=""></script>
<script src="/_next/static/chunks/1sb_tzsts-xox.js" defer=""></script>
<script src="/_next/static/<buildId>/_buildManifest.js" defer=""></script>
<script src="/_next/static/<buildId>/_ssgManifest.js" defer=""></script>
<script src="/_next/static/<buildId>/_clientMiddlewareManifest.js" defer=""></script>
</head>
<body class="tea-shop">
<div id="__next">
<nav><a href="/">Teas</a><input aria-label="Search teas" value=""></nav>
<main>
<h1>Mint tea</h1>
<p>$3 a box</p>
</main>
</div>
<script id="__NEXT_DATA__" type="application/json">…</script>
</body>
</html>Lines 5 to 11 of pages/teas/[id].tsx export getStaticPaths. Line 6 reads the teas, and line 8 turns each into { params: { id: … } }. Line 9 says what happens to every other id. getStaticProps, on lines 13 to 17, now gets params: line 14 looks up the tea, and line 15 returns notFound: true for an id the shop doesn't have. The Terminal tab lists /teas/[id] with ● /teas/green, ● /teas/mint and ● /teas/lemon under it: the build ran getStaticProps three times. The Network tab shows /teas/mint as the build made it, and /teas/oolong as a 404.
The list was made during the build. So what happens when the shop adds a tea after it? Each button below is a version of the page with a different fallback on line 9, built with real Next.js. The recorder then saved oolong and jasmine through the content system (a signed POST to /api/catalog that rewrites data/teas.json, explained in the ISR section) and visited /teas/oolong twice. Before you click, guess which version makes the first visitor wait, and which one shows a loading message.
pages/teas/[id].tsxNext.jspages/teas/[id].tsximport Head from "next/head";import { useRouter } from "next/router";import { getTea, getTeas, type Tea } from "@/lib/teas"; export async function getStaticPaths() { const teas = await getTeas(); return { paths: teas.map((tea) => ({ params: { id: tea.id } })), fallback: false, };} export async function getStaticProps({ params }: { params: { id: string } }) { const tea = await getTea(params.id); if (!tea) return { notFound: true }; return { props: { tea } };} export default function TeaPage({ tea }: { tea: Tea }) { const router = useRouter(); if (router.isFallback) return <p>Loading the tea…</p>; return ( <> <Head> <title>{`${tea.name} | Tea shop`}</title> </Head> <h1>{tea.name}</h1> <p>{`$${tea.price} a box`}</p> </> );}GET /teas/oolong404
- Response headers
Cache-Controlprivate, no-cache, no-store, max-age=0, must-revalidateX-Powered-ByNext.jsContent-Typetext/html; charset=utf-8VaryAccept-Encoding
The HTML that arrived, one element per line, with the code of its 11 scripts left out
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" data-next-head="">
<meta name="viewport" content="width=device-width" data-next-head="">
<title data-next-head="">404: This page could not be found</title>
<link rel="preload" href="/_next/static/chunks/3lqpx571i07yu.css" as="style">
<link rel="stylesheet" href="/_next/static/chunks/3lqpx571i07yu.css" data-n-g="">
<noscript data-n-css=""></noscript>
<script>…</script>
<script src="/_next/static/chunks/3t_68x1ty_491.js" defer=""></script>
<script src="/_next/static/chunks/1_2oau8-1i67e.js" defer=""></script>
<script src="/_next/static/chunks/43cz2ihhg0zvk.js" defer=""></script>
<script src="/_next/static/chunks/10_fcp04d3__p.js" defer=""></script>
<script src="/_next/static/chunks/turbopack-3-ry682tkyiqk.js" defer=""></script>
<script src="/_next/static/chunks/3kty7e_bozsbp.js" defer=""></script>
<script src="/_next/static/2ntU1RI8aBwEdU8ZUfQrI/_buildManifest.js" defer=""></script>
<script src="/_next/static/2ntU1RI8aBwEdU8ZUfQrI/_ssgManifest.js" defer=""></script>
<script src="/_next/static/2ntU1RI8aBwEdU8ZUfQrI/_clientMiddlewareManifest.js" defer=""></script>
</head>
<body class="tea-shop">
<div id="__next">
<nav><a href="/">Teas</a><input aria-label="Search teas" value=""></nav>
<main>
<div style="font-family:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center">
<div style="line-height:48px">
<style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style>
<h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding-right:23px;font-size:24px;font-weight:500;vertical-align:top">404</h1>
<div style="display:inline-block">
<h2 style="font-size:14px;font-weight:400;line-height:28px">This page could not be found<!-- -->.</h2>
</div>
</div>
</div>
</main>
</div>
<script id="__NEXT_DATA__" type="application/json">…</script>
</body>
</html>Line 9 says the paths the build listed are all there is. Oolong was added after the build, so /teas/oolong answered 404, though it's in data/teas.json now. Only a new build adds its page.
With false, the build's list is final: oolong answered 404, though it was in data/teas.json. With true, the first request got a fallback page at once: the page's component ran with no props and router.isFallback set, so line 21 showed Loading the tea…. In the browser, the page then asks for /_next/data/<buildId>/teas/oolong.json, waits while Next.js runs getStaticProps, and shows the tea. The recorder's Chrome did that for jasmine: open the Steps tab of the fallback: true version to see its JSON request. With "blocking", the first visitor waited and got the whole page, like a page made for that request. In both, Next.js stored the new page, and the next visit was a HIT. Only the true version's build wrote one more file, teas/[id].html in its Terminal tab: the fallback page itself, ready before any visitor asks.
/teas/oolong, a tea added after the build. false answers 404 until the next build. true sends the fallback page at once and fills it in from the JSON. "blocking" waits for the whole page. Both store the page for the next visit.Each row is one setting, and the two visits to oolong:
| fallback | The first visit to /teas/oolong | The next visit |
|---|---|---|
fallback: false | 404, the not-found page, even though oolong is in data/teas.json now. | 404 too, until a new build lists oolong. |
fallback: true | The fallback page at once, Loading the tea…, with MISS. In the browser, the page asks for oolong's JSON, then shows the tea. | The stored page, whole, with HIT. |
fallback: "blocking" | A wait while getStaticProps runs, then the whole page, with MISS. No loading state. | The stored page, whole, with HIT. |
Two details are easy to miss. With fallback: true, the fallback page answers 200 even for a tea that doesn't exist: in the recording, /teas/earl-grey got Loading the tea… with 200, and only its JSON answered 404, with {"notFound":true}. And the docs say search engine bots never get a fallback page, and neither does a visitor who arrives by a Link: both wait for the whole page, as with "blocking".
getServerSideProps: a page for each request
The cart is different for every visitor, so no build can make it. getServerSideProps runs on the server for every request, and gets the request itself:
pages/cart.tsximport type { GetServerSidePropsContext } from "next";import { getTea } from "@/lib/teas"; export async function getServerSideProps({ req }: GetServerSidePropsContext) { const tea = await getTea(req.cookies.cart ?? ""); const french = req.headers["accept-language"]?.startsWith("fr") ?? false; return { props: { greeting: french ? "Bonjour" : "Hello", item: tea?.name ?? null } };} type CartProps = { greeting: string; item: string | null }; export default function CartPage({ greeting, item }: CartProps) { return ( <> <h1>{greeting}, here is your cart</h1> <p>{item ? `In your cart: ${item}` : "Your cart is empty."}</p> </> );}GET /cart200
- Sent with the request
cookiecart=mintaccept-languagefr-FR,fr;q=0.9
- Response headers
Cache-Controlprivate, no-cache, no-store, max-age=0, must-revalidateX-Powered-ByNext.jsContent-Typetext/html; charset=utf-8VaryAccept-Encoding
The HTML that arrived, one element per line, with the code of its 11 scripts left out
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" data-next-head="">
<meta name="viewport" content="width=device-width" data-next-head="">
<link rel="preload" href="/_next/static/chunks/3lqpx571i07yu.css" as="style">
<link rel="stylesheet" href="/_next/static/chunks/3lqpx571i07yu.css" data-n-g="">
<noscript data-n-css=""></noscript>
<script>…</script>
<script src="/_next/static/chunks/3t_68x1ty_491.js" defer=""></script>
<script src="/_next/static/chunks/1_2oau8-1i67e.js" defer=""></script>
<script src="/_next/static/chunks/43cz2ihhg0zvk.js" defer=""></script>
<script src="/_next/static/chunks/10_fcp04d3__p.js" defer=""></script>
<script src="/_next/static/chunks/turbopack-3-ry682tkyiqk.js" defer=""></script>
<script src="/_next/static/chunks/2j059qeru_ayw.js" defer=""></script>
<script src="/_next/static/X4HrgPAHaZ8IGU5lmUsTm/_buildManifest.js" defer=""></script>
<script src="/_next/static/X4HrgPAHaZ8IGU5lmUsTm/_ssgManifest.js" defer=""></script>
<script src="/_next/static/X4HrgPAHaZ8IGU5lmUsTm/_clientMiddlewareManifest.js" defer=""></script>
</head>
<body class="tea-shop">
<div id="__next">
<nav><a href="/">Teas</a><input aria-label="Search teas" value=""></nav>
<main>
<h1>Bonjour<!-- -->, here is your cart</h1>
<p>In your cart: Mint tea</p>
</main>
</div>
<script id="__NEXT_DATA__" type="application/json">…</script>
</body>
</html>Line 4 of pages/cart.tsx exports getServerSideProps, which gets a context object. req in it is Node.js's request, with an extra cookies object. Line 5 reads the cart cookie and looks up that tea. Line 6 reads the visitor's Accept-Language header, the languages their browser prefers. Line 7 returns the props, and lines 12 to 19 show them.
The Terminal tab prints ƒ /cart: (Dynamic) server-rendered on demand. The Network tab shows two requests for the same address. Without a cookie, the page said Hello, here is your cart and Your cart is empty. With the cookie cart=mint and a French Accept-Language, it said Bonjour, here is your cart and In your cart: Mint tea. Both came with Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate, the default for a page with getServerSideProps: no cache may keep a copy. That's right for a cart. For a page that's the same for everyone, the function can set the header itself with res.setHeader("Cache-Control", …) to let a shared cache keep it, though the docs suggest ISR first. As with getStaticProps, the function stays on the server: the page's JavaScript holds here is your cart, from the component, and not accept-language, from line 6.
One thing about it matters a lot. Nothing is sent until getServerSideProps returns. Then the page renders, and the HTML goes out in one piece. If the function waits a second for a slow database, the visitor sees nothing from this page for that second. The data function runs before the page renders, so there's no loading state Next.js could send first. And when a visitor clicks a Link to a page with getServerSideProps, the docs say the browser asks the server for that page's JSON, and the function runs again.
getServerSideProps, nothing is sent until it returns, and then the whole page goes in one piece. In the App Router, the shell with Loading your cart… goes first, and the part that reads the cookie streams in after it.ISR with revalidate and res.revalidate
A page made during the build stays the same until the next build. The shop changes its prices a few times a day, and rebuilding the whole shop for that is slow. Incremental Static Regeneration (ISR) makes one stored page again after the build. The Pages Router got it in Next.js 9.5. Add one property to what getStaticProps returns:
pages/teas/[id].tsximport Head from "next/head";import { useRouter } from "next/router";import { getTea, getTeas, type Tea } from "@/lib/teas"; export async function getStaticPaths() { const teas = await getTeas(); return { paths: teas.map((tea) => ({ params: { id: tea.id } })), fallback: false, };} export async function getStaticProps({ params }: { params: { id: string } }) { const tea = await getTea(params.id); if (!tea) return { notFound: true }; return { props: { tea }, revalidate: 10 };} export default function TeaPage({ tea }: { tea: Tea }) { const router = useRouter(); if (router.isFallback) return <p>Loading the tea…</p>; return ( <> <Head> <title>{`${tea.name} | Tea shop`}</title> </Head> <h1>{tea.name}</h1> <p>{`$${tea.price} a box`}</p> </> );}GET /teas/green200
- Response headers
x-nextjs-cacheSTALECache-Controls-maxage=10, stale-while-revalidate=31535990X-Powered-ByNext.jsContent-Typetext/html; charset=utf-8VaryAccept-Encoding
The HTML that arrived, one element per line, with the code of its 11 scripts left out
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" data-next-head="">
<meta name="viewport" content="width=device-width" data-next-head="">
<title data-next-head="">Green tea | Tea shop</title>
<link rel="preload" href="/_next/static/chunks/3lqpx571i07yu.css" as="style">
<link rel="stylesheet" href="/_next/static/chunks/3lqpx571i07yu.css" data-n-g="">
<noscript data-n-css=""></noscript>
<script>…</script>
<script src="/_next/static/chunks/3t_68x1ty_491.js" defer=""></script>
<script src="/_next/static/chunks/1_2oau8-1i67e.js" defer=""></script>
<script src="/_next/static/chunks/43cz2ihhg0zvk.js" defer=""></script>
<script src="/_next/static/chunks/10_fcp04d3__p.js" defer=""></script>
<script src="/_next/static/chunks/turbopack-3-ry682tkyiqk.js" defer=""></script>
<script src="/_next/static/chunks/1sb_tzsts-xox.js" defer=""></script>
<script src="/_next/static/Cu5QrgDRZ0olLlOnYlPTq/_buildManifest.js" defer=""></script>
<script src="/_next/static/Cu5QrgDRZ0olLlOnYlPTq/_ssgManifest.js" defer=""></script>
<script src="/_next/static/Cu5QrgDRZ0olLlOnYlPTq/_clientMiddlewareManifest.js" defer=""></script>
</head>
<body class="tea-shop">
<div id="__next">
<nav><a href="/">Teas</a><input aria-label="Search teas" value=""></nav>
<main>
<h1>Green tea</h1>
<p>$4 a box</p>
</main>
</div>
<script id="__NEXT_DATA__" type="application/json">…</script>
</body>
</html>Line 16 of pages/teas/[id].tsx now returns revalidate: 10: a stored tea page may be made again once it's older than 10 seconds. The Terminal tab adds two columns, Revalidate and Expire, with 10s and 1y on the /teas/[id] line: 10 seconds to revalidate, and a year before a stored page expires. The other new files are the content system's side, and two helpers:
pages/api/catalog.tsis the content system's Save: a signedPOSTrewritesdata/teas.json. Writing a file stands in for a database; it only works on one server.pages/api/revalidate.tsis a webhook, which the next part explains.pages/api/wait.tsandpages/api/stored.tsexist only for the recording. The first answers after 11 seconds, so time passes. The second lists the stored copies of a page and whether each holds a text; withwait=yes, it answers only once one does. With them, nothing in the recording depends on how fast the computer was.
Read the Network tab from the top. Each request comes after the one before has answered:
- A signed call to the webhook made
/teas/greenagain at a known moment, so the 10 seconds start there. - The next two visits got
$4 a boxwithx-nextjs-cache: HIT. - The editor saved $5, and
/api/waitlet 11 seconds pass./api/storedthen found no stored copy with $5: time alone made nothing. - The first visit after that got
$4 a boxat once, withx-nextjs-cache: STALE, and started a new render in the background. - Once
/api/stored?…&wait=yesfound the new page, the next visit got$5 a box, aHIT.
This is stale-while-revalidate: the visitor who arrives after the 10 seconds gets the old page, and their request starts the new one. The first request after the lifetime starts the render; nothing happens on a timer. So "the second visit is fresh" isn't a promise: a visit is fresh only once the new page has been stored, and on a busy site that's a moment after it started. The header said so too: Cache-Control: s-maxage=10, stale-while-revalidate=31535990. The new page was stored in .next/server/route-cache/PAGES/…/$/teas/green.html, beside the build's .next/server/pages/teas/green.html, which still said $4; those folders are internals of Next.js 16.4.3.
On demand, from a signed webhook
A lifetime is a guess. When the content system knows the moment a price changed, it can call a webhook, and the page is made again right then. In the Pages Router, an API route does this with res.revalidate:
pages/api/revalidate.tsimport type { NextApiRequest, NextApiResponse } from "next";import { readBody } from "@/lib/body";import { isSigned } from "@/lib/signature"; export const config = { api: { bodyParser: false } }; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const body = await readBody(req); if (req.method !== "POST" || !isSigned(body, req.headers["x-signature"])) { return res.status(401).json({ error: "Bad signature" }); } await res.revalidate("/teas/green"); return res.json({ revalidated: true });}GET /teas/green200
- Response headers
x-nextjs-cacheHITCache-Controls-maxage=31536000X-Powered-ByNext.jsContent-Typetext/html; charset=utf-8VaryAccept-Encoding
The HTML that arrived, one element per line, with the code of its 11 scripts left out
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" data-next-head="">
<meta name="viewport" content="width=device-width" data-next-head="">
<title data-next-head="">Green tea | Tea shop</title>
<link rel="preload" href="/_next/static/chunks/3lqpx571i07yu.css" as="style">
<link rel="stylesheet" href="/_next/static/chunks/3lqpx571i07yu.css" data-n-g="">
<noscript data-n-css=""></noscript>
<script>…</script>
<script src="/_next/static/chunks/3t_68x1ty_491.js" defer=""></script>
<script src="/_next/static/chunks/1_2oau8-1i67e.js" defer=""></script>
<script src="/_next/static/chunks/43cz2ihhg0zvk.js" defer=""></script>
<script src="/_next/static/chunks/10_fcp04d3__p.js" defer=""></script>
<script src="/_next/static/chunks/turbopack-3-ry682tkyiqk.js" defer=""></script>
<script src="/_next/static/chunks/1sb_tzsts-xox.js" defer=""></script>
<script src="/_next/static/WAEMBhyJMW2yUqsv-fBxY/_buildManifest.js" defer=""></script>
<script src="/_next/static/WAEMBhyJMW2yUqsv-fBxY/_ssgManifest.js" defer=""></script>
<script src="/_next/static/WAEMBhyJMW2yUqsv-fBxY/_clientMiddlewareManifest.js" defer=""></script>
</head>
<body class="tea-shop">
<div id="__next">
<nav><a href="/">Teas</a><input aria-label="Search teas" value=""></nav>
<main>
<h1>Green tea</h1>
<p>$5 a box</p>
</main>
</div>
<script id="__NEXT_DATA__" type="application/json">…</script>
</body>
</html>An API route is a public address: anyone who finds it can call it. So the webhook checks who is asking before it does anything:
- Line 5 of
pages/api/revalidate.tsturns off Next.js's body parser, and line 8 reads the body as the exact bytes that arrived, withreadBodyfromlib/body.ts. The signature is checked over those bytes, not over text or parsed JSON made from them. - Line 9 accepts only a
POSTwhosex-signatureheader is the HMAC of that exact body, made with the shop's signing key.isSignedinlib/signature.tsis the check from Route Handlers, over bytes: lines 4 to 8 decode the key, 32 random bytes written as base64, and refuse to run with a key of any other length; line 12 accepts only 64 hex characters; line 15 compares the two signatures as bytes of the same length withtimingSafeEqual. Line 10 of the webhook answers401to anything else. - Line 13 calls
res.revalidate("/teas/green"). It makes the page at that exact path again, right away, and stores it.
The key in the example's .env is fixed for the recording only, so its signatures can be checked. A real app generates its own, with openssl rand -base64 32, and keeps it in a secret manager or the host's environment variables, never in the repository. The docs' own example compares a secret from the address with an environment variable. A signature of the body is safer: the key never travels, and a changed body fails the check. In the Network tab, the save alone changed nothing, and the visit after it still got $4. The call without a signature got 401, {"error":"Bad signature"}. The signed call answered {"revalidated":true}, and the next visit got $5 a box as a HIT: res.revalidate had made the page during the webhook's call, so the visitor didn't wait. A page without revalidate, like this example's, can still be made again this way. Pass the real path, like /teas/green, not /teas/[id].
Fetching in the browser with SWR
Some data changes by the minute and is fine to show a moment late, like how many boxes of a tea are left. The page around it can stay static, and the browser can ask for the number after the page loads. SWR is a small React library for this, from the team behind Next.js; its name comes from stale-while-revalidate. Here the tea page shows the stock with SWR:
components/stock.tsximport useSWR from "swr"; async function fetcher(url: string) { const response = await fetch(url); return response.json();} export default function Stock({ id }: { id: string }) { const { data } = useSWR<{ left: number }>(`/api/stock?id=${id}`, fetcher); return <p>{data ? `Boxes left: ${data.left}` : "Checking stock…"}</p>;}Each step was taken in real Chrome against next start. Pick one to see the page after it.
$4 a box
Boxes left: 12
- Focus
- nothing: the page itself
- Scrolled
- 0 px down
Requests the page made during this step (3), without the files it loaded
GET/teas/green200a whole pageGET/api/stock?id=green200application/jsonGET/_next/data/<buildId>/index.json200application/json
In components/stock.tsx, lines 3 to 6 are the fetcher: the function SWR calls to load a key. Line 9 calls useSWR with the key /api/stock?id=green, the address of an API route that reads data/stock.json. Until the data arrives, data is undefined, so line 10 shows Checking stock…; then it shows Boxes left: 12. The Steps tab shows the browser's request for /api/stock?id=green, and the page after it.
The Network tab shows the other half: the HTML that /teas/green sent says Checking stock…, and the build still printed ● /teas/green. The tea's name and price are made during the build; only the number is fetched in the browser. So a visitor without JavaScript, and a search engine that doesn't run it, never see the number. A Pages Router component can call useSWR without "use client", because every component there also runs in the browser. Fetching in the browser teaches SWR in the App Router, where it goes in a Client Component.
The same jobs in the App Router
The App Router has none of these functions. A page there is a Server Component that can wait for its data while it renders, and Next.js decides how each part is made from what the code reads. The jobs stay the same. Each row is one Pages Router tool, and what does its job in an App Router app with Cache Components on, as new projects have it:
| In the Pages Router | In the App Router, with Cache Components |
|---|---|
getStaticProps | An async Server Component that awaits its data. Data that should be stored goes in a function with "use cache". |
getStaticPaths and its paths | generateStaticParams, which returns [{ id: "green" }, …]. |
fallback: true | The default for unlisted params: a loading shell from the Suspense fallback, then the page, stored for the next visit. |
fallback: "blocking" | export const ensureStatic = "navigation" (16.4): an unlisted param's first visit waits for the whole page. |
fallback: false | No direct equivalent with Cache Components: dynamicParams = false works only without them, and an unlisted but valid id still renders on request. notFound() rejects only an id whose data is missing. |
revalidate: 10 | cacheLife({ revalidate: 10 }) in the cached function, or a profile like cacheLife("hours"). |
res.revalidate("/teas/green") | revalidatePath("/teas/green"), or revalidateTag for data with a cacheTag, in a Route Handler. |
getServerSideProps with req | await cookies() and await headers() in a Server Component inside Suspense, so the shell streams first. |
| SWR in the page | SWR in a Client Component, a file that starts with "use client". |
The 'use cache' directive, generateStaticParams, Incremental Static Regeneration and Request-time APIs teach each one; generateStaticParams records what an unlisted tea gets with and without Cache Components, which is why fallback: false has no one-line replacement. The biggest change is in the getServerSideProps row: a request-time part no longer holds up the whole page. Here is the cart in each router, asked for with the same cookie and header:
pages/cart.tsximport type { GetServerSidePropsContext } from "next";import { getTea } from "@/lib/teas"; export async function getServerSideProps({ req }: GetServerSidePropsContext) { const tea = await getTea(req.cookies.cart ?? ""); const french = req.headers["accept-language"]?.startsWith("fr") ?? false; return { props: { greeting: french ? "Bonjour" : "Hello", item: tea?.name ?? null } };} type CartProps = { greeting: string; item: string | null }; export default function CartPage({ greeting, item }: CartProps) { return ( <> <h1>{greeting}, here is your cart</h1> <p>{item ? `In your cart: ${item}` : "Your cart is empty."}</p> </> );}GET /cart200
- Sent with the request
cookiecart=mintaccept-languagefr-FR,fr;q=0.9
- Response headers
Cache-Controlprivate, no-cache, no-store, max-age=0, must-revalidateX-Powered-ByNext.jsContent-Typetext/html; charset=utf-8VaryAccept-Encoding
The HTML that arrived, one element per line, with the code of its 11 scripts left out
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" data-next-head="">
<meta name="viewport" content="width=device-width" data-next-head="">
<link rel="preload" href="/_next/static/chunks/3lqpx571i07yu.css" as="style">
<link rel="stylesheet" href="/_next/static/chunks/3lqpx571i07yu.css" data-n-g="">
<noscript data-n-css=""></noscript>
<script>…</script>
<script src="/_next/static/chunks/3t_68x1ty_491.js" defer=""></script>
<script src="/_next/static/chunks/1_2oau8-1i67e.js" defer=""></script>
<script src="/_next/static/chunks/43cz2ihhg0zvk.js" defer=""></script>
<script src="/_next/static/chunks/10_fcp04d3__p.js" defer=""></script>
<script src="/_next/static/chunks/turbopack-3-ry682tkyiqk.js" defer=""></script>
<script src="/_next/static/chunks/2j059qeru_ayw.js" defer=""></script>
<script src="/_next/static/X4HrgPAHaZ8IGU5lmUsTm/_buildManifest.js" defer=""></script>
<script src="/_next/static/X4HrgPAHaZ8IGU5lmUsTm/_ssgManifest.js" defer=""></script>
<script src="/_next/static/X4HrgPAHaZ8IGU5lmUsTm/_clientMiddlewareManifest.js" defer=""></script>
</head>
<body class="tea-shop">
<div id="__next">
<nav><a href="/">Teas</a><input aria-label="Search teas" value=""></nav>
<main>
<h1>Bonjour<!-- -->, here is your cart</h1>
<p>In your cart: Mint tea</p>
</main>
</div>
<script id="__NEXT_DATA__" type="application/json">…</script>
</body>
</html>Nothing is sent until getServerSideProps on lines 4 to 8 returns. Then the whole page goes in one piece: in the Network tab, the cart is already in <main>.
The App Router's cart reads the cookie and the header with await cookies() and await headers(), on lines 6 and 7 of app/cart/page.tsx, inside the Suspense on lines 19 to 21. Its build printed ◐ /cart, a static shell with dynamic content streamed in. In its Network tab, the HTML's <main> holds Loading your cart…, and the cart comes later in the same response. The Pages Router's HTML has the cart in <main> from the start, because nothing was sent until the cart was ready.
import { Suspense } from "react";
import { cookies, headers } from "next/headers";
import { getTea } from "@/lib/teas";
async function Cart() {
const tea = await getTea((await cookies()).get("cart")?.value ?? "");
const french = (await headers()).get("accept-language")?.startsWith("fr") ?? false;
return (
<>
<h1>{french ? "Bonjour" : "Hello"}, here is your cart</h1>
<p>{tea ? `In your cart: ${tea.name}` : "Your cart is empty."}</p>
</>
);
}
export default function CartPage() {
return (
<main>
<Suspense fallback={<p>Loading your cart…</p>}>
<Cart />
</Suspense>
</main>
);
}
Lines 5 to 14 are the part that needs the request, a Server Component called Cart. Lines 16 to 24 are the page, and only lines 19 to 21 wait for Cart. The docs' migration guide shows these moves with fetch options, from the caching model before Cache Components; with Cache Components on, "use cache" and cacheLife say the same thing. Migrating to the App Router moves the shop over one route at a time.
One function for each page of the shop
In a Pages Router app, each page picks one way to get its data, and its parts can add SWR. Ask two questions about each page: does it depend on who is asking, and how late may its data be? Sort these pages of the shop:
- The About page, changed twice a year.
- Brewing guides for each tea, written once.
- A tea's page, whose price the shop edits a few times a day.
- A tea's reviews, a few new ones each hour.
- The cart, read from the visitor's cookie.
- An order history behind a login.
- Boxes left of a tea, changing by the minute.
- An order's status while it's brewing.
Sort each page by the way it should get its data in a Pages Router app.
A page picks one: the home page below exports both functions, and the build stops. Open the Terminal tab for the error.
pages/index.tsximport Head from "next/head";import Link from "next/link";import { getTeas, type Tea } from "@/lib/teas"; export async function getStaticProps() { const teas = await getTeas(); return { props: { teas } };} export async function getServerSideProps() { return { props: { teas: await getTeas() } };} export default function Home({ teas }: { teas: Tea[] }) { return ( <> <Head> <title>Tea shop</title> </Head> <h1>Tea shop</h1> <ul> {teas.map((tea) => ( <li key={tea.id}> <Link href={`/teas/${tea.id}`}>{tea.name}</Link> </li> ))} </ul> </> );}$ next build▲ Next.js 16.4.3 (Turbopack) Running TypeScript ... Finished TypeScript ... Creating an optimized production build ... > Build error occurredError: Turbopack build failed with 1 error:./pages/index.tsx:10:23Error: You can not use getStaticProps or getStaticPaths with getServerSideProps. To use SSG, please remove getServerSideProps 8 | } 9 |> 10 | export async function getServerSideProps() { | ^^^^^^^^^^^^^^^^^^ 11 | return { props: { teas: await getTeas() } }; 12 | } 13 | Ecmascript file had an error at ignore-listed framesLines 10 to 12 of pages/index.tsx add getServerSideProps, and the build fails with You can not use getStaticProps or getStaticPaths with getServerSideProps. When most of a page is the same for everyone and one part isn't, keep the page static and fetch that part in the browser, as the tea page does with its stock.
Common mix-ups
- "getStaticProps runs in the browser on navigation." No. A click loads the JSON the build wrote;
getStaticPropsruns during the build, or on the server for ISR and fallback pages. - "Props from getStaticProps stay on the server." No. The function's code does. Its props are sent to the browser, in the page's HTML and in its JSON file, so they must never hold a secret.
- "revalidate: 10 makes a new page every 10 seconds." No. Nothing happens until a request comes after the 10 seconds. That request gets the old page, marked
STALE, and starts the render. - "With fallback: false, a tea saved after the build gets its page right away." No. Only the next build adds it; until then, its address answers
404. - "fallback: true sends a 404 for a tea that doesn't exist." Not at first. The fallback page answers
200; only the JSON it asks for answers404. - "getServerSideProps can stream a loading state." No. The page is sent once the function returns. A loading state needs the App Router's
Suspense, or a part fetched in the browser. - "A secret in the webhook's address is enough." No. A signature of the body is safer: the key never travels, and a changed body fails the check.
Practice exercises
These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.
This is what the build printed for the tea shop. Which symbol marks the home page, /?
Route (pages)
┌ ● /
├ /_app
├ ○ /404
└ /teas/[id]
├ ● /teas/green
├ ● /teas/mint
└ ● /teas/lemon
○ (Static) prerendered as static content
● (SSG) prerendered as static HTML (uses getStaticProps)●: (SSG) prerendered as static HTML (uses getStaticProps). The page's HTML and its JSON were made during the build with what getStaticProps returned.
A visitor on this home page clicks Mint tea. Which address does the browser ask the server for?
import Head from "next/head";
import Link from "next/link";
import { getTeas, type Tea } from "@/lib/teas";
export async function getStaticProps() {
const teas = await getTeas();
return { props: { teas } };
}
export default function Home({ teas }: { teas: Tea[] }) {
return (
<>
<Head>
<title>Tea shop</title>
</Head>
<h1>Tea shop</h1>
<ul>
{teas.map((tea) => (
<li key={tea.id}>
<Link href={`/teas/${tea.id}`}>{tea.name}</Link>
</li>
))}
</ul>
</>
);
}
/_next/data/<buildId>/teas/mint.json, the JSON file the build wrote, where <buildId> is the build's id (the router adds ?id=mint). React renders TeaPage in the browser with its pageProps.
The editor adds oolong to data/teas.json after the build. What does /teas/oolong answer with this page?
import Head from "next/head";
import { useRouter } from "next/router";
import { getTea, getTeas, type Tea } from "@/lib/teas";
export async function getStaticPaths() {
const teas = await getTeas();
return {
paths: teas.map((tea) => ({ params: { id: tea.id } })),
fallback: false,
};
}
export async function getStaticProps({ params }: { params: { id: string } }) {
const tea = await getTea(params.id);
if (!tea) return { notFound: true };
return { props: { tea } };
}
export default function TeaPage({ tea }: { tea: Tea }) {
const router = useRouter();
if (router.isFallback) return <p>Loading the tea…</p>;
return (
<>
<Head>
<title>{`${tea.name} | Tea shop`}</title>
</Head>
<h1>{tea.name}</h1>
<p>{`$${tea.price} a box`}</p>
</>
);
}
404. The build listed green, mint and lemon, and fallback: false on line 9 says that list is final. The recorded /teas/oolong answered 404 after oolong was saved.
Add the function this page needs, so it shows the tea in the visitor's cart cookie. Which object holds the cookie?
import { getTea } from "@/lib/teas";
// Show the tea whose id is in the visitor's "cart" cookie.
export default function CartPage({ item }: { item: string | null }) {
return <p>{item ? `In your cart: ${item}` : "Your cart is empty."}</p>;
}
import type { GetServerSidePropsContext } from "next";
import { getTea } from "@/lib/teas";
export async function getServerSideProps({ req }: GetServerSidePropsContext) {
const tea = await getTea(req.cookies.cart ?? "");
const french = req.headers["accept-language"]?.startsWith("fr") ?? false;
return { props: { greeting: french ? "Bonjour" : "Hello", item: tea?.name ?? null } };
}
type CartProps = { greeting: string; item: string | null };
export default function CartPage({ greeting, item }: CartProps) {
return (
<>
<h1>{greeting}, here is your cart</h1>
<p>{item ? `In your cart: ${item}` : "Your cart is empty."}</p>
</>
);
}
Export getServerSideProps and read req.cookies.cart, as line 5 does, then return the tea's name as a prop. The build prints ƒ /cart, and the recorded request with cart=mint shows In your cart: Mint tea.
/teas/green was made with Green tea at $4. Then the editor saved $5, and 11 seconds passed with no visitors. Which price does the next visit to /teas/green get?
import Head from "next/head";
import { useRouter } from "next/router";
import { getTea, getTeas, type Tea } from "@/lib/teas";
export async function getStaticPaths() {
const teas = await getTeas();
return {
paths: teas.map((tea) => ({ params: { id: tea.id } })),
fallback: false,
};
}
export async function getStaticProps({ params }: { params: { id: string } }) {
const tea = await getTea(params.id);
if (!tea) return { notFound: true };
return { props: { tea }, revalidate: 10 };
}
export default function TeaPage({ tea }: { tea: Tea }) {
const router = useRouter();
if (router.isFallback) return <p>Loading the tea…</p>;
return (
<>
<Head>
<title>{`${tea.name} | Tea shop`}</title>
</Head>
<h1>{tea.name}</h1>
<p>{`$${tea.price} a box`}</p>
</>
);
}
$4. The page is stale, so that visit gets the stored page at once, marked STALE, and a new render starts in the background. Visits after the new page is stored get $5.
Write app/cart/page.tsx for this cart, so the page doesn't wait for the cookie before it sends anything. What does the HTML show before the cart arrives?
import type { GetServerSidePropsContext } from "next";
import { getTea } from "@/lib/teas";
export async function getServerSideProps({ req }: GetServerSidePropsContext) {
const tea = await getTea(req.cookies.cart ?? "");
const french = req.headers["accept-language"]?.startsWith("fr") ?? false;
return { props: { greeting: french ? "Bonjour" : "Hello", item: tea?.name ?? null } };
}
type CartProps = { greeting: string; item: string | null };
export default function CartPage({ greeting, item }: CartProps) {
return (
<>
<h1>{greeting}, here is your cart</h1>
<p>{item ? `In your cart: ${item}` : "Your cart is empty."}</p>
</>
);
}
import { Suspense } from "react";
import { cookies, headers } from "next/headers";
import { getTea } from "@/lib/teas";
async function Cart() {
const tea = await getTea((await cookies()).get("cart")?.value ?? "");
const french = (await headers()).get("accept-language")?.startsWith("fr") ?? false;
return (
<>
<h1>{french ? "Bonjour" : "Hello"}, here is your cart</h1>
<p>{tea ? `In your cart: ${tea.name}` : "Your cart is empty."}</p>
</>
);
}
export default function CartPage() {
return (
<main>
<Suspense fallback={<p>Loading your cart…</p>}>
<Cart />
</Suspense>
</main>
);
}
The fallback, Loading your cart…, is in the build's shell and goes first; the cart streams in. The recorded App Router cart's HTML holds Loading your cart… in <main>, and the build printed ◐ /cart.
Check your understanding
For each question, ask when the function runs, and what the visitor's request gets.
Question 1 of 7When does
getStaticPropsinpages/index.tsxrun, with norevalidate?Choose an answer to see the explanation.
Question 2 of 7The visitor is on this home page and clicks Mint tea. What does the browser ask the server for?
Read the code, then predictTSXimport Head from "next/head"; import Link from "next/link"; import { getTeas, type Tea } from "@/lib/teas"; export async function getStaticProps() { const teas = await getTeas(); return { props: { teas } }; } export default function Home({ teas }: { teas: Tea[] }) { return ( <> <Head> <title>Tea shop</title> </Head> <h1>Tea shop</h1> <ul> {teas.map((tea) => ( <li key={tea.id}> <Link href={`/teas/${tea.id}`}>{tea.name}</Link> </li> ))} </ul> </> ); }Choose an answer to see the explanation.
Question 3 of 7This page has
fallback: true, and oolong was added after the build. What does a visitor's first request for/teas/oolongget?Read the code, then predictTSXimport Head from "next/head"; import { useRouter } from "next/router"; import { getTea, getTeas, type Tea } from "@/lib/teas"; export async function getStaticPaths() { const teas = await getTeas(); return { paths: teas.map((tea) => ({ params: { id: tea.id } })), fallback: true, }; } export async function getStaticProps({ params }: { params: { id: string } }) { const tea = await getTea(params.id); if (!tea) return { notFound: true }; return { props: { tea } }; } export default function TeaPage({ tea }: { tea: Tea }) { const router = useRouter(); if (router.isFallback) return <p>Loading the tea…</p>; return ( <> <Head> <title>{`${tea.name} | Tea shop`}</title> </Head> <h1>{tea.name}</h1> <p>{`$${tea.price} a box`}</p> </> ); }Choose an answer to see the explanation.
Question 4 of 7Why can't
getStaticPropsread the visitor's cookie?Choose an answer to see the explanation.
Question 5 of 7The tea pages have
revalidate: 10. More than 10 seconds have passed since/teas/greenwas made. What does the next visit get?Choose an answer to see the explanation.
Question 6 of 7What do lines 9 to 11 of this webhook protect against?
Read the code, then predictTypeScriptimport type { NextApiRequest, NextApiResponse } from "next"; import { readBody } from "@/lib/body"; import { isSigned } from "@/lib/signature"; export const config = { api: { bodyParser: false } }; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const body = await readBody(req); if (req.method !== "POST" || !isSigned(body, req.headers["x-signature"])) { return res.status(401).json({ error: "Bad signature" }); } await res.revalidate("/teas/green"); return res.json({ revalidated: true }); }Choose an answer to see the explanation.
Question 7 of 7This is the cart page. What does the visitor see before its HTML arrives?
Read the code, then predictTSXimport type { GetServerSidePropsContext } from "next"; import { getTea } from "@/lib/teas"; export async function getServerSideProps({ req }: GetServerSidePropsContext) { const tea = await getTea(req.cookies.cart ?? ""); const french = req.headers["accept-language"]?.startsWith("fr") ?? false; return { props: { greeting: french ? "Bonjour" : "Hello", item: tea?.name ?? null } }; } type CartProps = { greeting: string; item: string | null }; export default function CartPage({ greeting, item }: CartProps) { return ( <> <h1>{greeting}, here is your cart</h1> <p>{item ? `In your cart: ${item}` : "Your cart is empty."}</p> </> ); }Choose an answer to see the explanation.
Key takeaways
- A Pages Router page gets its data from a function it exports, and its props go to the component on the server and in the browser.
getStaticPropsruns during the build (●) and writes an HTML file and a JSON file. A visit gets the HTML; aLinkclick gets the JSON.getStaticPathslists the dynamic pages to build.fallbackdecides what other ids get:404, a fallback page then the data, or a wait for the whole page.getServerSidePropsruns for each request withreqandres(ƒ), and nothing is sent until it returns.revalidatemakes a stored page again after a request finds it stale; that request gets the old page.res.revalidatein a signed webhook makes it again right away.- In the App Router, Server Components with
"use cache",generateStaticParams,cacheLifeand request-time APIs inSuspensedo these jobs.
Remember the one-liner.
In the Pages Router, when a page's data function runs decides how the page is made: once for the build, again for ISR, or for every request.
These links point at release v16.4.3. In lines 886 to 899 of server/render.tsx, Next.js calls getStaticProps with params and a revalidateReason (build, stale or on-demand), and no request. At lines 1106 to 1120, it calls getServerSideProps with req, res and query.
Coming next: Migrating to the App Router. It runs both routers side by side, and moves the tea shop over one route at a time.