Skip to content

Commit c7079c9

Browse files
committed
docs: dynamic routes w/ gsP
1 parent 22cc5be commit c7079c9

File tree

3 files changed

+401
-1
lines changed

3 files changed

+401
-1
lines changed

docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,270 @@ export default async function Page(props: PageProps<'/[locale]'>) {
145145
- Since the `params` prop is a promise. You must use `async`/`await` or React's use function to access the values.
146146
- In version 14 and earlier, `params` was a synchronous prop. To help with backwards compatibility, you can still access it synchronously in Next.js 15, but this behavior will be deprecated in the future.
147147

148+
### With Cache Components
149+
150+
When using [Cache Components](/docs/app/getting-started/cache-components) with dynamic routes, params are runtime data that require special handling. During prerendering, Next.js doesn't know which params users will request (like `/blog/hello-world` or `/blog/123`), so you need to provide fallback UI using `<Suspense>` boundaries.
151+
152+
You should use [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params) to prerender your most popular routes at build time (e.g., `/blog/1`, `/blog/2`, `/blog/3`). This serves two purposes: it validates your route doesn't incorrectly access dynamic APIs like `cookies()` or `headers()`, and it creates static HTML files for instant loading of those specific routes. Any other routes will render on-demand when requested at runtime.
153+
154+
> **Good to know**: You can check the `X-Nextjs-Cache` response header to verify your caching strategy. It will show `HIT` or `MISS` for disk-cached routes, and won't appear for memory-only caching.
155+
156+
The sections below show different patterns, from simplest (all runtime) to most optimized (prerendered samples + caching).
157+
158+
#### Without `generateStaticParams`
159+
160+
The simplest approach. All params are runtime data.
161+
162+
**Properties:**
163+
164+
- **Build time**: No validation, no prerendering
165+
- **Prerendered params**: None
166+
- **Runtime params**: Reading params requires Suspense fallback UI (build fails without it), shell renders on-demand, UI updates when content resolves
167+
- **Caching**: Memory only (cleared on server restart)
168+
169+
```tsx filename="app/blog/[slug]/page.tsx"
170+
import { Suspense } from 'react'
171+
172+
export default async function Page({
173+
params,
174+
}: {
175+
params: Promise<{ slug: string }>
176+
}) {
177+
return (
178+
<div>
179+
<h1>Blog Post</h1>
180+
<Suspense fallback={<div>Loading...</div>}>
181+
<Content slug={params.then((p) => p.slug)} />
182+
</Suspense>
183+
</div>
184+
)
185+
}
186+
187+
async function Content({ slug }: { slug: Promise<string> }) {
188+
const { slug: resolvedSlug } = await slug
189+
return <article>{/* Your content */}</article>
190+
}
191+
```
192+
193+
#### With `generateStaticParams` + `<Suspense>`
194+
195+
Good for frequently updating content where you don't need disk persistence.
196+
197+
**Properties:**
198+
199+
- **Build time**: Validates route, prerenders samples (e.g., `/1`, `/2`, `/3`)
200+
- **Prerendered params**: Instant - served from disk
201+
- **Runtime params**: Shell renders immediately, UI updates when content resolves
202+
- **Caching**: Memory only (cleared on server restart)
203+
204+
```tsx filename="app/blog/[slug]/page.tsx"
205+
import { Suspense } from 'react'
206+
207+
export async function generateStaticParams() {
208+
return [{ slug: '1' }, { slug: '2' }, { slug: '3' }]
209+
}
210+
211+
export default async function Page({
212+
params,
213+
}: {
214+
params: Promise<{ slug: string }>
215+
}) {
216+
return (
217+
<div>
218+
<h1>Blog Post</h1>
219+
<Suspense fallback={<div>Loading...</div>}>
220+
<Content slug={params.then((p) => p.slug)} />
221+
</Suspense>
222+
</div>
223+
)
224+
}
225+
226+
async function Content({ slug }: { slug: Promise<string> }) {
227+
const { slug: resolvedSlug } = await slug
228+
return <article>{/* Your content */}</article>
229+
}
230+
```
231+
232+
#### With `generateStaticParams` + `<Suspense>` + `use cache` on page
233+
234+
Recommended for most use cases. Provides immediate UI for prerendered and previously visited routes, with disk persistence similar to ISR.
235+
236+
**Properties:**
237+
238+
- **Build time**: Validates route, prerenders samples
239+
- **Prerendered params**: Instant - served from disk
240+
- **Runtime params**: Shell renders immediately, UI updates when content resolves
241+
- **Caching**: Disk (persists across server restarts)
242+
243+
```tsx filename="app/blog/[slug]/page.tsx"
244+
import { Suspense } from 'react'
245+
import { cacheLife } from 'next/cache'
246+
247+
export async function generateStaticParams() {
248+
return [{ slug: '1' }, { slug: '2' }, { slug: '3' }]
249+
}
250+
251+
export default async function Page({
252+
params,
253+
}: {
254+
params: Promise<{ slug: string }>
255+
}) {
256+
'use cache'
257+
cacheLife('days')
258+
return (
259+
<div>
260+
<h1>Blog Post</h1>
261+
<Suspense fallback={<div>Loading...</div>}>
262+
<Content slug={params.then((p) => p.slug)} />
263+
</Suspense>
264+
</div>
265+
)
266+
}
267+
268+
async function Content({ slug }: { slug: Promise<string> }) {
269+
const { slug: resolvedSlug } = await slug
270+
return <article>{/* Your content */}</article>
271+
}
272+
```
273+
274+
#### With `generateStaticParams` + `use cache` on page (no Suspense)
275+
276+
Full page waits but cached to disk. Good for less-frequently-changing content.
277+
278+
**Properties:**
279+
280+
- **Build time**: Validates route, prerenders samples
281+
- **Prerendered params**: Instant - served from disk
282+
- **Runtime params**: Full page waits to render, then cached to disk
283+
- **Caching**: Disk (persists across server restarts)
284+
285+
```tsx filename="app/blog/[slug]/page.tsx"
286+
import { cacheLife } from 'next/cache'
287+
288+
export async function generateStaticParams() {
289+
return [{ slug: '1' }, { slug: '2' }, { slug: '3' }]
290+
}
291+
292+
export default async function Page({
293+
params,
294+
}: {
295+
params: Promise<{ slug: string }>
296+
}) {
297+
'use cache'
298+
cacheLife('days')
299+
const { slug } = await params
300+
return <div>{/* Full page content */}</div>
301+
}
302+
```
303+
304+
#### Parent Suspense boundaries override disk caching
305+
306+
When you have a parent Suspense boundary that wraps the page, it takes precedence over page-level `use cache` for runtime params. This happens with [`loading.tsx`](/docs/app/api-reference/file-conventions/loading) or when a layout wraps children in a Suspense boundary to defer to request time. This means:
307+
308+
- **Build-time params** (from `generateStaticParams`) are still fully prerendered and cached to disk
309+
- **Runtime params** will always use the parent fallback UI and stream, with memory caching only
310+
- No disk files are created for runtime params, even with `use cache` on the page
311+
- The `X-Nextjs-Cache` header will not appear for runtime params
312+
313+
```tsx filename="app/blog/[slug]/loading.tsx"
314+
export default function Loading() {
315+
return <div>Loading post...</div>
316+
}
317+
```
318+
319+
```tsx filename="app/blog/[slug]/page.tsx"
320+
export async function generateStaticParams() {
321+
return [{ slug: '1' }, { slug: '2' }, { slug: '3' }]
322+
}
323+
324+
export default async function Page({
325+
params,
326+
}: {
327+
params: Promise<{ slug: string }>
328+
}) {
329+
'use cache' // This will be overridden by parent loading.tsx for runtime params
330+
const { slug } = await params
331+
return <div>Content for {slug}</div>
332+
}
333+
```
334+
335+
In this example:
336+
337+
- `/blog/1`, `/blog/2`, `/blog/3` are fully cached to disk at build time
338+
- `/blog/10` (runtime param) shows `loading.tsx` fallback, then streams the page content with memory caching only
339+
340+
If you need disk caching for runtime params, avoid parent Suspense boundaries and use page-level `use cache` with direct param access (no Suspense in the page).
341+
342+
#### Recommended pattern
343+
344+
For the most flexible approach that provides full page ISR for runtime params:
345+
346+
1. **Place static UI in the layout** - Content that doesn't depend on params goes in the layout, which is always part of the static shell
347+
2. **Use `use cache` on the page component** - This enables disk caching for the full page (persists across server restarts)
348+
3. **Read params within a Suspense boundary** - This enables streaming and provides good UX with fallback UI
349+
350+
```tsx filename="app/blog/[slug]/layout.tsx"
351+
export default function Layout({ children }: { children: React.ReactNode }) {
352+
return (
353+
<div>
354+
{/* Static navigation - always in the shell */}
355+
<nav>
356+
<a href="/">Home</a> | <a href="/blog">Blog</a>
357+
</nav>
358+
<main>{children}</main>
359+
</div>
360+
)
361+
}
362+
```
363+
364+
```tsx filename="app/blog/[slug]/page.tsx"
365+
import { Suspense } from 'react'
366+
import { cacheLife } from 'next/cache'
367+
368+
export async function generateStaticParams() {
369+
return [{ slug: '1' }, { slug: '2' }, { slug: '3' }]
370+
}
371+
372+
export default async function Page({
373+
params,
374+
}: {
375+
params: Promise<{ slug: string }>
376+
}) {
377+
'use cache'
378+
cacheLife('days')
379+
return (
380+
<div>
381+
<h1>Blog Post</h1>
382+
<Suspense fallback={<div>Loading content...</div>}>
383+
<BlogContent slug={params.then((p) => p.slug)} />
384+
</Suspense>
385+
</div>
386+
)
387+
}
388+
389+
async function BlogContent({ slug }: { slug: Promise<string> }) {
390+
const resolvedSlug = await slug
391+
const response = await fetch(`https://api.vercel.app/blog/${resolvedSlug}`)
392+
const data = await response.json()
393+
return (
394+
<article>
395+
<h2>{data.title}</h2>
396+
<p>{data.content}</p>
397+
</article>
398+
)
399+
}
400+
```
401+
402+
This pattern gives you:
403+
404+
- **Static shell** from layout and page-level elements outside Suspense
405+
- **Efficient client-side navigation** - Static layout UI is reused when navigating between different params (e.g., `/blog/1` to `/blog/2`), only the page content re-renders
406+
- **Streaming with Suspense** for dynamic content (good UX)
407+
- **Full page ISR** - Runtime params generate `.html` files cached to disk (persists across server restarts)
408+
- **Immediate UI** for prerendered and previously visited routes
409+
- **Build-time prerendering** for sampled params (from `generateStaticParams`)
410+
- **On-demand rendering and caching** for runtime params
411+
148412
## Examples
149413

150414
### With `generateStaticParams`
@@ -172,3 +436,56 @@ export async function generateStaticParams() {
172436
```
173437

174438
When using `fetch` inside the `generateStaticParams` function, the requests are [automatically deduplicated](/docs/app/guides/caching#request-memoization). This avoids multiple network calls for the same data Layouts, Pages, and other `generateStaticParams` functions, speeding up build time.
439+
440+
### Dynamic GET Route Handlers with `generateStaticParams`
441+
442+
`generateStaticParams` also works with dynamic [Route Handlers](/docs/app/api-reference/file-conventions/route) to statically generate API responses at build time:
443+
444+
```ts filename="app/api/posts/[id]/route.ts" switcher
445+
export async function generateStaticParams() {
446+
const posts = await fetch('https://api.vercel.app/blog').then((res) =>
447+
res.json()
448+
)
449+
450+
return posts.map((post: { id: number }) => ({
451+
id: String(post.id),
452+
}))
453+
}
454+
455+
export async function GET(
456+
request: Request,
457+
{ params }: { params: Promise<{ id: string }> }
458+
) {
459+
const { id } = await params
460+
const posts = await fetch('https://api.vercel.app/blog').then((res) =>
461+
res.json()
462+
)
463+
const post = posts.find((p: { id: number }) => p.id === Number(id))
464+
465+
return Response.json(post)
466+
}
467+
```
468+
469+
```js filename="app/api/posts/[id]/route.js" switcher
470+
export async function generateStaticParams() {
471+
const posts = await fetch('https://api.vercel.app/blog').then((res) =>
472+
res.json()
473+
)
474+
475+
return posts.map((post) => ({
476+
id: String(post.id),
477+
}))
478+
}
479+
480+
export async function GET(request, { params }) {
481+
const { id } = await params
482+
const posts = await fetch('https://api.vercel.app/blog').then((res) =>
483+
res.json()
484+
)
485+
const post = posts.find((p) => p.id === Number(id))
486+
487+
return Response.json(post)
488+
}
489+
```
490+
491+
In this example, the route handlers for all blog post IDs (1-25) returned by `generateStaticParams` will be statically generated at build time. Requests to other IDs will be handled dynamically at request time.

docs/01-app/03-api-reference/03-file-conventions/route.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,14 @@ export async function GET(request, { params }) {
334334
| `app/items/[slug]/route.js` | `/items/b` | `Promise<{ slug: 'b' }>` |
335335
| `app/items/[slug]/route.js` | `/items/c` | `Promise<{ slug: 'c' }>` |
336336

337+
#### Static Generation with `generateStaticParams`
338+
339+
You can use [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params) with dynamic Route Handlers to statically generate responses at build time for specified params, while handling other params dynamically at request time.
340+
341+
When using [Cache Components](/docs/app/getting-started/cache-components), you can combine `generateStaticParams` with `use cache` to enable data caching for both prerendered and runtime params.
342+
343+
See the [generateStaticParams with Route Handlers](/docs/app/api-reference/functions/generate-static-params#with-route-handlers) documentation for examples and details.
344+
337345
### URL Query Parameters
338346

339347
The request object passed to the Route Handler is a `NextRequest` instance, which includes [some additional convenience methods](/docs/app/api-reference/functions/next-request#nexturl), such as those for more easily handling query parameters.

0 commit comments

Comments
 (0)