From c7079c99cb95c3e38711e8433c1fd70f996d5e87 Mon Sep 17 00:00:00 2001 From: Joseph Chamochumbi Date: Sat, 22 Nov 2025 02:33:51 +0100 Subject: [PATCH 01/13] docs: dynamic routes w/ gsP --- .../03-file-conventions/dynamic-routes.mdx | 317 ++++++++++++++++++ .../03-file-conventions/route.mdx | 8 + .../04-functions/generate-static-params.mdx | 77 ++++- 3 files changed, 401 insertions(+), 1 deletion(-) diff --git a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx index 27e84fe4fd17fb..de0764e7f24546 100644 --- a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx +++ b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx @@ -145,6 +145,270 @@ export default async function Page(props: PageProps<'/[locale]'>) { - Since the `params` prop is a promise. You must use `async`/`await` or React's use function to access the values. - 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. +### With Cache Components + +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 `` boundaries. + +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. + +> **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. + +The sections below show different patterns, from simplest (all runtime) to most optimized (prerendered samples + caching). + +#### Without `generateStaticParams` + +The simplest approach. All params are runtime data. + +**Properties:** + +- **Build time**: No validation, no prerendering +- **Prerendered params**: None +- **Runtime params**: Reading params requires Suspense fallback UI (build fails without it), shell renders on-demand, UI updates when content resolves +- **Caching**: Memory only (cleared on server restart) + +```tsx filename="app/blog/[slug]/page.tsx" +import { Suspense } from 'react' + +export default async function Page({ + params, +}: { + params: Promise<{ slug: string }> +}) { + return ( +
+

Blog Post

+ Loading...
}> + p.slug)} /> +
+ + ) +} + +async function Content({ slug }: { slug: Promise }) { + const { slug: resolvedSlug } = await slug + return
{/* Your content */}
+} +``` + +#### With `generateStaticParams` + `` + +Good for frequently updating content where you don't need disk persistence. + +**Properties:** + +- **Build time**: Validates route, prerenders samples (e.g., `/1`, `/2`, `/3`) +- **Prerendered params**: Instant - served from disk +- **Runtime params**: Shell renders immediately, UI updates when content resolves +- **Caching**: Memory only (cleared on server restart) + +```tsx filename="app/blog/[slug]/page.tsx" +import { Suspense } from 'react' + +export async function generateStaticParams() { + return [{ slug: '1' }, { slug: '2' }, { slug: '3' }] +} + +export default async function Page({ + params, +}: { + params: Promise<{ slug: string }> +}) { + return ( +
+

Blog Post

+ Loading...
}> + p.slug)} /> +
+ + ) +} + +async function Content({ slug }: { slug: Promise }) { + const { slug: resolvedSlug } = await slug + return
{/* Your content */}
+} +``` + +#### With `generateStaticParams` + `` + `use cache` on page + +Recommended for most use cases. Provides immediate UI for prerendered and previously visited routes, with disk persistence similar to ISR. + +**Properties:** + +- **Build time**: Validates route, prerenders samples +- **Prerendered params**: Instant - served from disk +- **Runtime params**: Shell renders immediately, UI updates when content resolves +- **Caching**: Disk (persists across server restarts) + +```tsx filename="app/blog/[slug]/page.tsx" +import { Suspense } from 'react' +import { cacheLife } from 'next/cache' + +export async function generateStaticParams() { + return [{ slug: '1' }, { slug: '2' }, { slug: '3' }] +} + +export default async function Page({ + params, +}: { + params: Promise<{ slug: string }> +}) { + 'use cache' + cacheLife('days') + return ( +
+

Blog Post

+ Loading...
}> + p.slug)} /> +
+ + ) +} + +async function Content({ slug }: { slug: Promise }) { + const { slug: resolvedSlug } = await slug + return
{/* Your content */}
+} +``` + +#### With `generateStaticParams` + `use cache` on page (no Suspense) + +Full page waits but cached to disk. Good for less-frequently-changing content. + +**Properties:** + +- **Build time**: Validates route, prerenders samples +- **Prerendered params**: Instant - served from disk +- **Runtime params**: Full page waits to render, then cached to disk +- **Caching**: Disk (persists across server restarts) + +```tsx filename="app/blog/[slug]/page.tsx" +import { cacheLife } from 'next/cache' + +export async function generateStaticParams() { + return [{ slug: '1' }, { slug: '2' }, { slug: '3' }] +} + +export default async function Page({ + params, +}: { + params: Promise<{ slug: string }> +}) { + 'use cache' + cacheLife('days') + const { slug } = await params + return
{/* Full page content */}
+} +``` + +#### Parent Suspense boundaries override disk caching + +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: + +- **Build-time params** (from `generateStaticParams`) are still fully prerendered and cached to disk +- **Runtime params** will always use the parent fallback UI and stream, with memory caching only +- No disk files are created for runtime params, even with `use cache` on the page +- The `X-Nextjs-Cache` header will not appear for runtime params + +```tsx filename="app/blog/[slug]/loading.tsx" +export default function Loading() { + return
Loading post...
+} +``` + +```tsx filename="app/blog/[slug]/page.tsx" +export async function generateStaticParams() { + return [{ slug: '1' }, { slug: '2' }, { slug: '3' }] +} + +export default async function Page({ + params, +}: { + params: Promise<{ slug: string }> +}) { + 'use cache' // This will be overridden by parent loading.tsx for runtime params + const { slug } = await params + return
Content for {slug}
+} +``` + +In this example: + +- `/blog/1`, `/blog/2`, `/blog/3` are fully cached to disk at build time +- `/blog/10` (runtime param) shows `loading.tsx` fallback, then streams the page content with memory caching only + +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). + +#### Recommended pattern + +For the most flexible approach that provides full page ISR for runtime params: + +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 +2. **Use `use cache` on the page component** - This enables disk caching for the full page (persists across server restarts) +3. **Read params within a Suspense boundary** - This enables streaming and provides good UX with fallback UI + +```tsx filename="app/blog/[slug]/layout.tsx" +export default function Layout({ children }: { children: React.ReactNode }) { + return ( +
+ {/* Static navigation - always in the shell */} + +
{children}
+
+ ) +} +``` + +```tsx filename="app/blog/[slug]/page.tsx" +import { Suspense } from 'react' +import { cacheLife } from 'next/cache' + +export async function generateStaticParams() { + return [{ slug: '1' }, { slug: '2' }, { slug: '3' }] +} + +export default async function Page({ + params, +}: { + params: Promise<{ slug: string }> +}) { + 'use cache' + cacheLife('days') + return ( +
+

Blog Post

+ Loading content...
}> + p.slug)} /> + + + ) +} + +async function BlogContent({ slug }: { slug: Promise }) { + const resolvedSlug = await slug + const response = await fetch(`https://api.vercel.app/blog/${resolvedSlug}`) + const data = await response.json() + return ( +
+

{data.title}

+

{data.content}

+
+ ) +} +``` + +This pattern gives you: + +- **Static shell** from layout and page-level elements outside Suspense +- **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 +- **Streaming with Suspense** for dynamic content (good UX) +- **Full page ISR** - Runtime params generate `.html` files cached to disk (persists across server restarts) +- **Immediate UI** for prerendered and previously visited routes +- **Build-time prerendering** for sampled params (from `generateStaticParams`) +- **On-demand rendering and caching** for runtime params + ## Examples ### With `generateStaticParams` @@ -172,3 +436,56 @@ export async function generateStaticParams() { ``` 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. + +### Dynamic GET Route Handlers with `generateStaticParams` + +`generateStaticParams` also works with dynamic [Route Handlers](/docs/app/api-reference/file-conventions/route) to statically generate API responses at build time: + +```ts filename="app/api/posts/[id]/route.ts" switcher +export async function generateStaticParams() { + const posts = await fetch('https://api.vercel.app/blog').then((res) => + res.json() + ) + + return posts.map((post: { id: number }) => ({ + id: String(post.id), + })) +} + +export async function GET( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params + const posts = await fetch('https://api.vercel.app/blog').then((res) => + res.json() + ) + const post = posts.find((p: { id: number }) => p.id === Number(id)) + + return Response.json(post) +} +``` + +```js filename="app/api/posts/[id]/route.js" switcher +export async function generateStaticParams() { + const posts = await fetch('https://api.vercel.app/blog').then((res) => + res.json() + ) + + return posts.map((post) => ({ + id: String(post.id), + })) +} + +export async function GET(request, { params }) { + const { id } = await params + const posts = await fetch('https://api.vercel.app/blog').then((res) => + res.json() + ) + const post = posts.find((p) => p.id === Number(id)) + + return Response.json(post) +} +``` + +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. diff --git a/docs/01-app/03-api-reference/03-file-conventions/route.mdx b/docs/01-app/03-api-reference/03-file-conventions/route.mdx index 8c3ed020ffbc51..c1f513a1d4ea54 100644 --- a/docs/01-app/03-api-reference/03-file-conventions/route.mdx +++ b/docs/01-app/03-api-reference/03-file-conventions/route.mdx @@ -334,6 +334,14 @@ export async function GET(request, { params }) { | `app/items/[slug]/route.js` | `/items/b` | `Promise<{ slug: 'b' }>` | | `app/items/[slug]/route.js` | `/items/c` | `Promise<{ slug: 'c' }>` | +#### Static Generation with `generateStaticParams` + +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. + +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. + +See the [generateStaticParams with Route Handlers](/docs/app/api-reference/functions/generate-static-params#with-route-handlers) documentation for examples and details. + ### URL Query Parameters 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. diff --git a/docs/01-app/03-api-reference/04-functions/generate-static-params.mdx b/docs/01-app/03-api-reference/04-functions/generate-static-params.mdx index aecceb326115f5..c8620a9577ad14 100644 --- a/docs/01-app/03-api-reference/04-functions/generate-static-params.mdx +++ b/docs/01-app/03-api-reference/04-functions/generate-static-params.mdx @@ -5,6 +5,12 @@ description: API reference for the generateStaticParams function. The `generateStaticParams` function can be used in combination with [dynamic route segments](/docs/app/api-reference/file-conventions/dynamic-routes) to [**statically generate**](/docs/app/guides/caching#static-rendering) routes at build time instead of on-demand at request time. +`generateStaticParams` can be used with: + +- [Pages](/docs/app/api-reference/file-conventions/page) (`page.tsx`/`page.js`) +- [Layouts](/docs/app/api-reference/file-conventions/layout) (`layout.tsx`/`layout.js`) +- [Route Handlers](/docs/app/api-reference/file-conventions/route) (`route.ts`/`route.js`) + ```tsx filename="app/blog/[slug]/page.tsx" switcher // Return a list of `params` to populate the [slug] dynamic segment export async function generateStaticParams() { @@ -54,6 +60,14 @@ export default async function Page({ params }) { > - During revalidation (ISR), `generateStaticParams` will not be called again. > - `generateStaticParams` replaces the [`getStaticPaths`](/docs/pages/api-reference/functions/get-static-paths) function in the Pages Router. +### With Cache Components + +When using [Cache Components](/docs/app/getting-started/cache-components) with dynamic routes, `generateStaticParams` must return **at least one param**. Empty arrays cause a [build error](/docs/messages/empty-generate-static-params). This allows Cache Components to validate your route doesn't incorrectly access `cookies()`, `headers()`, or `searchParams` at runtime. + +> **Good to know**: If you don't know the actual param values at build time, you can return a placeholder param (e.g., `[{ slug: '__placeholder__' }]`) for validation, then handle it in your page with `notFound()`. However, this prevents build time validation from working effectively and may cause runtime errors. + +See the [dynamic routes section](/docs/app/api-reference/file-conventions/dynamic-routes#with-cache-components) for detailed walkthroughs. + ## Parameters `options.params` (optional) @@ -291,12 +305,73 @@ export async function generateStaticParams() { } ``` -> **Good to know:** You must always return an array from `generateStaticParams`, even if it's empty. Otherwise, the route will be dynamically rendered. +> **Good to know:** +> +> - You must always return an array from `generateStaticParams`, even if it's empty. Otherwise, the route will be dynamically rendered. +> - **With Cache Components**, returning an empty array is **not allowed** and will cause a [build error](/docs/messages/empty-generate-static-params). You must return at least one param for build-time validation. ```jsx filename="app/changelog/[slug]/page.js" export const dynamic = 'force-static' ``` +### With Route Handlers + +You can use `generateStaticParams` with [Route Handlers](/docs/app/api-reference/file-conventions/route) to statically generate API responses at build time: + +```ts filename="app/api/posts/[id]/route.ts" switcher +export async function generateStaticParams() { + return [{ id: '1' }, { id: '2' }, { id: '3' }] +} + +export async function GET( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params + // This will be statically generated for IDs 1, 2, and 3 + return Response.json({ id, title: `Post ${id}` }) +} +``` + +```js filename="app/api/posts/[id]/route.js" switcher +export async function generateStaticParams() { + return [{ id: '1' }, { id: '2' }, { id: '3' }] +} + +export async function GET(request, { params }) { + const { id } = await params + // This will be statically generated for IDs 1, 2, and 3 + return Response.json({ id, title: `Post ${id}` }) +} +``` + +#### With Cache Components + +When using [Cache Components](/docs/app/getting-started/cache-components), combine with `use cache` for optimal caching: + +```ts filename="app/api/posts/[id]/route.ts" +export async function generateStaticParams() { + return [{ id: '1' }, { id: '2' }, { id: '3' }] +} + +async function getPost(id: Promise) { + 'use cache' + const resolvedId = await id + const response = await fetch(`https://api.example.com/posts/${resolvedId}`) + return response.json() +} + +export async function GET( + request: Request, + { params }: RouteContext<'/api/posts/[id]'> +) { + const post = await getPost(params.then((p) => p.id)) + return Response.json(post) +} +``` + +See the [Route Handlers documentation](/docs/app/api-reference/file-conventions/route#static-generation-with-generatestaticparams) for more details. + ### Disable rendering for unspecified paths To prevent unspecified paths from being statically rendered at runtime, add the `export const dynamicParams = false` option in a route segment. When this config option is used, only paths provided by `generateStaticParams` will be served, and unspecified routes will 404 or match (in the case of [catch-all routes](/docs/app/api-reference/file-conventions/dynamic-routes#catch-all-segments)). From ee9d41347764e754030bb2e8b2a70a408944f23a Mon Sep 17 00:00:00 2001 From: Joseph Chamochumbi Date: Sat, 22 Nov 2025 02:41:39 +0100 Subject: [PATCH 02/13] docs: fix headings in dynamic routes --- .../03-api-reference/03-file-conventions/dynamic-routes.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx index de0764e7f24546..0f1308ae5ec0cb 100644 --- a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx +++ b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx @@ -190,7 +190,7 @@ async function Content({ slug }: { slug: Promise }) { } ``` -#### With `generateStaticParams` + `` +#### With `generateStaticParams` and Suspense Good for frequently updating content where you don't need disk persistence. @@ -229,7 +229,7 @@ async function Content({ slug }: { slug: Promise }) { } ``` -#### With `generateStaticParams` + `` + `use cache` on page +#### With `generateStaticParams`, Suspense, and `use cache` on page Recommended for most use cases. Provides immediate UI for prerendered and previously visited routes, with disk persistence similar to ISR. @@ -271,7 +271,7 @@ async function Content({ slug }: { slug: Promise }) { } ``` -#### With `generateStaticParams` + `use cache` on page (no Suspense) +#### With `generateStaticParams` and `use cache` on page Full page waits but cached to disk. Good for less-frequently-changing content. From a3a65f486ea6a17c4dbd1040bf53899b4ae66c3f Mon Sep 17 00:00:00 2001 From: Joseph Chamochumbi Date: Sat, 22 Nov 2025 02:53:29 +0100 Subject: [PATCH 03/13] docs: simplify snippets and use RouteContext --- .../03-file-conventions/dynamic-routes.mdx | 32 ++++--------------- .../04-functions/generate-static-params.mdx | 2 +- 2 files changed, 7 insertions(+), 27 deletions(-) diff --git a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx index 0f1308ae5ec0cb..1f062e9966ef87 100644 --- a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx +++ b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx @@ -443,13 +443,7 @@ When using `fetch` inside the `generateStaticParams` function, the requests are ```ts filename="app/api/posts/[id]/route.ts" switcher export async function generateStaticParams() { - const posts = await fetch('https://api.vercel.app/blog').then((res) => - res.json() - ) - - return posts.map((post: { id: number }) => ({ - id: String(post.id), - })) + return [{ id: '1' }, { id: '2' }, { id: '3' }] } export async function GET( @@ -457,34 +451,20 @@ export async function GET( { params }: { params: Promise<{ id: string }> } ) { const { id } = await params - const posts = await fetch('https://api.vercel.app/blog').then((res) => - res.json() - ) - const post = posts.find((p: { id: number }) => p.id === Number(id)) - - return Response.json(post) + // Fetch and return post data + return Response.json({ id, title: `Post ${id}` }) } ``` ```js filename="app/api/posts/[id]/route.js" switcher export async function generateStaticParams() { - const posts = await fetch('https://api.vercel.app/blog').then((res) => - res.json() - ) - - return posts.map((post) => ({ - id: String(post.id), - })) + return [{ id: '1' }, { id: '2' }, { id: '3' }] } export async function GET(request, { params }) { const { id } = await params - const posts = await fetch('https://api.vercel.app/blog').then((res) => - res.json() - ) - const post = posts.find((p) => p.id === Number(id)) - - return Response.json(post) + // Fetch and return post data + return Response.json({ id, title: `Post ${id}` }) } ``` diff --git a/docs/01-app/03-api-reference/04-functions/generate-static-params.mdx b/docs/01-app/03-api-reference/04-functions/generate-static-params.mdx index c8620a9577ad14..34c5fb2e01cb9c 100644 --- a/docs/01-app/03-api-reference/04-functions/generate-static-params.mdx +++ b/docs/01-app/03-api-reference/04-functions/generate-static-params.mdx @@ -325,7 +325,7 @@ export async function generateStaticParams() { export async function GET( request: Request, - { params }: { params: Promise<{ id: string }> } + { params }: RouteContext<'/api/posts/[id]'> ) { const { id } = await params // This will be statically generated for IDs 1, 2, and 3 From b08fa944a6192d33d033522dff43272efd01bbe8 Mon Sep 17 00:00:00 2001 From: Joseph Chamochumbi Date: Sat, 22 Nov 2025 02:56:23 +0100 Subject: [PATCH 04/13] docs: dynamic routes tweaks --- .../03-api-reference/03-file-conventions/dynamic-routes.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx index 1f062e9966ef87..dcb9ed97f0cd6c 100644 --- a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx +++ b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx @@ -402,8 +402,8 @@ async function BlogContent({ slug }: { slug: Promise }) { This pattern gives you: - **Static shell** from layout and page-level elements outside Suspense -- **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 -- **Streaming with Suspense** for dynamic content (good UX) +- **Efficient client-side navigation** - Static layout UI is reused when navigating between different params (e.g., `/blog/1` to `/blog/2`) +- **Fallback UI with Suspense** - Provides immediate feedback while content loads, fallback elements are replaced when content resolves - **Full page ISR** - Runtime params generate `.html` files cached to disk (persists across server restarts) - **Immediate UI** for prerendered and previously visited routes - **Build-time prerendering** for sampled params (from `generateStaticParams`) @@ -448,7 +448,7 @@ export async function generateStaticParams() { export async function GET( request: Request, - { params }: { params: Promise<{ id: string }> } + { params }: RouteContext<'/api/posts/[id]'> ) { const { id } = await params // Fetch and return post data From 5bed7a3a7f943f97fefed3fcc28f54a25e574bd2 Mon Sep 17 00:00:00 2001 From: Joseph Chamochumbi Date: Tue, 25 Nov 2025 13:07:14 +0100 Subject: [PATCH 05/13] docs: fix await slug into resolvedSlug --- .../03-api-reference/03-file-conventions/dynamic-routes.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx index dcb9ed97f0cd6c..c359fc8605bfbf 100644 --- a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx +++ b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx @@ -185,7 +185,7 @@ export default async function Page({ } async function Content({ slug }: { slug: Promise }) { - const { slug: resolvedSlug } = await slug + const resolvedSlug = await slug return
{/* Your content */}
} ``` @@ -224,7 +224,7 @@ export default async function Page({ } async function Content({ slug }: { slug: Promise }) { - const { slug: resolvedSlug } = await slug + const resolvedSlug = await slug return
{/* Your content */}
} ``` @@ -266,7 +266,7 @@ export default async function Page({ } async function Content({ slug }: { slug: Promise }) { - const { slug: resolvedSlug } = await slug + const resolvedSlug = await slug return
{/* Your content */}
} ``` From c27ad1f2ee98f928533b54c12ca504fe6f2bef23 Mon Sep 17 00:00:00 2001 From: Joseph Chamochumbi Date: Tue, 25 Nov 2025 13:16:32 +0100 Subject: [PATCH 06/13] docs: blog post IDs 1,2, and 3 --- .../03-api-reference/03-file-conventions/dynamic-routes.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx index c359fc8605bfbf..76bdb8952e39e1 100644 --- a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx +++ b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx @@ -468,4 +468,4 @@ export async function GET(request, { params }) { } ``` -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. +In this example, the route handlers for all blog post IDs (1, 2 and 3) returned by `generateStaticParams` will be statically generated at build time. Requests to other IDs will be handled dynamically at request time. From bbdfa072e2e452cfabfce4f283e3689ff7ad9d79 Mon Sep 17 00:00:00 2001 From: Joseph Chamochumbi Date: Tue, 25 Nov 2025 14:10:55 +0100 Subject: [PATCH 07/13] docs: gsp w/ cache components --- .../03-file-conventions/dynamic-routes.mdx | 174 ++---------------- 1 file changed, 11 insertions(+), 163 deletions(-) diff --git a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx index 76bdb8952e39e1..97b001261efc53 100644 --- a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx +++ b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx @@ -153,14 +153,12 @@ You should use [`generateStaticParams`](/docs/app/api-reference/functions/genera > **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. -The sections below show different patterns, from simplest (all runtime) to most optimized (prerendered samples + caching). +The sections below show different patterns, from simplest (all runtime) to most optimized (prerendered samples, and caching). We recommend wrapping param access in Suspense boundaries to provide meaningful loading states. #### Without `generateStaticParams` The simplest approach. All params are runtime data. -**Properties:** - - **Build time**: No validation, no prerendering - **Prerendered params**: None - **Runtime params**: Reading params requires Suspense fallback UI (build fails without it), shell renders on-demand, UI updates when content resolves @@ -169,11 +167,7 @@ The simplest approach. All params are runtime data. ```tsx filename="app/blog/[slug]/page.tsx" import { Suspense } from 'react' -export default async function Page({ - params, -}: { - params: Promise<{ slug: string }> -}) { +export default async function Page({ params }: PageProps<'/blog/[slug]'>) { return (

Blog Post

@@ -190,11 +184,9 @@ async function Content({ slug }: { slug: Promise }) { } ``` -#### With `generateStaticParams` and Suspense - -Good for frequently updating content where you don't need disk persistence. +#### With `generateStaticParams` -**Properties:** +Enables build-time validation and prerendering of popular routes, with memory caching for runtime params. - **Build time**: Validates route, prerenders samples (e.g., `/1`, `/2`, `/3`) - **Prerendered params**: Instant - served from disk @@ -208,11 +200,7 @@ export async function generateStaticParams() { return [{ slug: '1' }, { slug: '2' }, { slug: '3' }] } -export default async function Page({ - params, -}: { - params: Promise<{ slug: string }> -}) { +export default async function Page({ params }: PageProps<'/blog/[slug]'>) { return (

Blog Post

@@ -229,11 +217,9 @@ async function Content({ slug }: { slug: Promise }) { } ``` -#### With `generateStaticParams`, Suspense, and `use cache` on page - -Recommended for most use cases. Provides immediate UI for prerendered and previously visited routes, with disk persistence similar to ISR. +#### With `generateStaticParams` and `use cache` on page -**Properties:** +Provides immediate UI for prerendered and previously visited routes, with disk persistence similar to ISR. - **Build time**: Validates route, prerenders samples - **Prerendered params**: Instant - served from disk @@ -248,11 +234,7 @@ export async function generateStaticParams() { return [{ slug: '1' }, { slug: '2' }, { slug: '3' }] } -export default async function Page({ - params, -}: { - params: Promise<{ slug: string }> -}) { +export default async function Page({ params }: PageProps<'/blog/[slug]'>) { 'use cache' cacheLife('days') return ( @@ -271,143 +253,9 @@ async function Content({ slug }: { slug: Promise }) { } ``` -#### With `generateStaticParams` and `use cache` on page - -Full page waits but cached to disk. Good for less-frequently-changing content. - -**Properties:** - -- **Build time**: Validates route, prerenders samples -- **Prerendered params**: Instant - served from disk -- **Runtime params**: Full page waits to render, then cached to disk -- **Caching**: Disk (persists across server restarts) - -```tsx filename="app/blog/[slug]/page.tsx" -import { cacheLife } from 'next/cache' - -export async function generateStaticParams() { - return [{ slug: '1' }, { slug: '2' }, { slug: '3' }] -} - -export default async function Page({ - params, -}: { - params: Promise<{ slug: string }> -}) { - 'use cache' - cacheLife('days') - const { slug } = await params - return
{/* Full page content */}
-} -``` - -#### Parent Suspense boundaries override disk caching - -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: - -- **Build-time params** (from `generateStaticParams`) are still fully prerendered and cached to disk -- **Runtime params** will always use the parent fallback UI and stream, with memory caching only -- No disk files are created for runtime params, even with `use cache` on the page -- The `X-Nextjs-Cache` header will not appear for runtime params - -```tsx filename="app/blog/[slug]/loading.tsx" -export default function Loading() { - return
Loading post...
-} -``` - -```tsx filename="app/blog/[slug]/page.tsx" -export async function generateStaticParams() { - return [{ slug: '1' }, { slug: '2' }, { slug: '3' }] -} - -export default async function Page({ - params, -}: { - params: Promise<{ slug: string }> -}) { - 'use cache' // This will be overridden by parent loading.tsx for runtime params - const { slug } = await params - return
Content for {slug}
-} -``` - -In this example: - -- `/blog/1`, `/blog/2`, `/blog/3` are fully cached to disk at build time -- `/blog/10` (runtime param) shows `loading.tsx` fallback, then streams the page content with memory caching only - -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). - -#### Recommended pattern - -For the most flexible approach that provides full page ISR for runtime params: - -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 -2. **Use `use cache` on the page component** - This enables disk caching for the full page (persists across server restarts) -3. **Read params within a Suspense boundary** - This enables streaming and provides good UX with fallback UI - -```tsx filename="app/blog/[slug]/layout.tsx" -export default function Layout({ children }: { children: React.ReactNode }) { - return ( -
- {/* Static navigation - always in the shell */} - -
{children}
-
- ) -} -``` - -```tsx filename="app/blog/[slug]/page.tsx" -import { Suspense } from 'react' -import { cacheLife } from 'next/cache' - -export async function generateStaticParams() { - return [{ slug: '1' }, { slug: '2' }, { slug: '3' }] -} - -export default async function Page({ - params, -}: { - params: Promise<{ slug: string }> -}) { - 'use cache' - cacheLife('days') - return ( -
-

Blog Post

- Loading content...
}> - p.slug)} /> - -
- ) -} - -async function BlogContent({ slug }: { slug: Promise }) { - const resolvedSlug = await slug - const response = await fetch(`https://api.vercel.app/blog/${resolvedSlug}`) - const data = await response.json() - return ( -
-

{data.title}

-

{data.content}

-
- ) -} -``` - -This pattern gives you: - -- **Static shell** from layout and page-level elements outside Suspense -- **Efficient client-side navigation** - Static layout UI is reused when navigating between different params (e.g., `/blog/1` to `/blog/2`) -- **Fallback UI with Suspense** - Provides immediate feedback while content loads, fallback elements are replaced when content resolves -- **Full page ISR** - Runtime params generate `.html` files cached to disk (persists across server restarts) -- **Immediate UI** for prerendered and previously visited routes -- **Build-time prerendering** for sampled params (from `generateStaticParams`) -- **On-demand rendering and caching** for runtime params +> **Good to know**: +> +> - Parent Suspense boundaries (like [`loading.tsx`](/docs/app/api-reference/file-conventions/loading) or a layout Suspense wrapping children) take precedence over page-level `use cache` for runtime params. While build-time params from `generateStaticParams` are still fully cached to disk, runtime params will use the parent fallback UI and stream with memory caching only - no disk files are created. ## Examples From ffb03b70a25ddba4e16eb422e8107a6440b5aa84 Mon Sep 17 00:00:00 2001 From: Joseph Chamochumbi Date: Tue, 25 Nov 2025 14:15:11 +0100 Subject: [PATCH 08/13] docs: sign post to dynamic routes from CC getting started --- docs/01-app/01-getting-started/06-cache-components.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/01-app/01-getting-started/06-cache-components.mdx b/docs/01-app/01-getting-started/06-cache-components.mdx index 54d13ded1d2062..24ea804e650ade 100644 --- a/docs/01-app/01-getting-started/06-cache-components.mdx +++ b/docs/01-app/01-getting-started/06-cache-components.mdx @@ -158,7 +158,7 @@ A specific type of dynamic data that requires request context, only available wh - [`cookies()`](/docs/app/api-reference/functions/cookies) - User's cookie data - [`headers()`](/docs/app/api-reference/functions/headers) - Request headers - [`searchParams`](/docs/app/api-reference/file-conventions/page#searchparams-optional) - URL query parameters -- [`params`](/docs/app/api-reference/file-conventions/page#params-optional) - Dynamic route parameters (unless at least one sample is provided via [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params)) +- [`params`](/docs/app/api-reference/file-conventions/page#params-optional) - Dynamic route parameters (unless at least one sample is provided via [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params)). See [Dynamic Routes with Cache Components](/docs/app/api-reference/file-conventions/dynamic-routes#with-cache-components) for detailed patterns. ```tsx filename="page.tsx" import { cookies, headers } from 'next/headers' From 134b325bcef3e1afa31d1fb239eccb5d5eff8a03 Mon Sep 17 00:00:00 2001 From: Joseph Chamochumbi Date: Tue, 25 Nov 2025 14:19:26 +0100 Subject: [PATCH 09/13] docs: keep cache components info groupped --- .../03-api-reference/04-functions/generate-static-params.mdx | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/01-app/03-api-reference/04-functions/generate-static-params.mdx b/docs/01-app/03-api-reference/04-functions/generate-static-params.mdx index 34c5fb2e01cb9c..f10c99aaa1662a 100644 --- a/docs/01-app/03-api-reference/04-functions/generate-static-params.mdx +++ b/docs/01-app/03-api-reference/04-functions/generate-static-params.mdx @@ -308,7 +308,6 @@ export async function generateStaticParams() { > **Good to know:** > > - You must always return an array from `generateStaticParams`, even if it's empty. Otherwise, the route will be dynamically rendered. -> - **With Cache Components**, returning an empty array is **not allowed** and will cause a [build error](/docs/messages/empty-generate-static-params). You must return at least one param for build-time validation. ```jsx filename="app/changelog/[slug]/page.js" export const dynamic = 'force-static' From 69d1d15b518eff95c293d346d12cd2e5ba68700a Mon Sep 17 00:00:00 2001 From: Joseph Chamochumbi Date: Tue, 25 Nov 2025 21:50:14 +0100 Subject: [PATCH 10/13] docs: gsp explicit static shell generation and prefetching --- .../03-file-conventions/dynamic-routes.mdx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx index 97b001261efc53..418960077208d7 100644 --- a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx +++ b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx @@ -149,12 +149,14 @@ export default async function Page(props: PageProps<'/[locale]'>) { 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 `` boundaries. -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. +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. -> **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. +When you use `generateStaticParams`, Next.js also generates a static shell where params are treated as dynamic. When users request params not prerendered at build time, Next.js immediately sends this static shell (containing content like headers, navigation, and layout), while param-dependent content updates the UI when it's ready. This provides a better user experience than waiting for the entire page to render. The sections below show different patterns, from simplest (all runtime) to most optimized (prerendered samples, and caching). We recommend wrapping param access in Suspense boundaries to provide meaningful loading states. +> **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. Your hosting provider might replace this header with their own version. + #### Without `generateStaticParams` The simplest approach. All params are runtime data. @@ -186,12 +188,12 @@ async function Content({ slug }: { slug: Promise }) { #### With `generateStaticParams` -Enables build-time validation and prerendering of popular routes, with memory caching for runtime params. +Enables build-time validation and prerendering of popular routes. Next.js generates both the prerendered param pages and a static shell (where params are treated as dynamic). When a runtime param is encountered, Next.js uses this static shell to render immediately while streaming param-dependent content. -- **Build time**: Validates route, prerenders samples (e.g., `/1`, `/2`, `/3`) +- **Build time**: Validates route, prerenders samples (e.g., `/1`, `/2`, `/3`), generates static shell - **Prerendered params**: Instant - served from disk -- **Runtime params**: Shell renders immediately, UI updates when content resolves -- **Caching**: Memory only (cleared on server restart) +- **Runtime params**: Static shell renders immediately, UI updates when param-dependent content resolves +- **Caching**: Memory only for runtime params (cleared on server restart) ```tsx filename="app/blog/[slug]/page.tsx" import { Suspense } from 'react' @@ -219,11 +221,11 @@ async function Content({ slug }: { slug: Promise }) { #### With `generateStaticParams` and `use cache` on page -Provides immediate UI for prerendered and previously visited routes, with disk persistence similar to ISR. +Provides immediate UI for prerendered and previously visited routes, with disk persistence similar to ISR. When a runtime param is encountered, Next.js uses the static shell if it has visible content, or prerenders the full page (ISR-style) if the shell is empty. -- **Build time**: Validates route, prerenders samples +- **Build time**: Validates route, prerenders samples, generates static shell - **Prerendered params**: Instant - served from disk -- **Runtime params**: Shell renders immediately, UI updates when content resolves +- **Runtime params**: Static shell renders immediately (if it has content), then streams param-dependent content. Full page is cached to disk after first visit. - **Caching**: Disk (persists across server restarts) ```tsx filename="app/blog/[slug]/page.tsx" @@ -255,7 +257,8 @@ async function Content({ slug }: { slug: Promise }) { > **Good to know**: > -> - Parent Suspense boundaries (like [`loading.tsx`](/docs/app/api-reference/file-conventions/loading) or a layout Suspense wrapping children) take precedence over page-level `use cache` for runtime params. While build-time params from `generateStaticParams` are still fully cached to disk, runtime params will use the parent fallback UI and stream with memory caching only - no disk files are created. +> - **Prefetching**: When navigating via ``, Next.js prefetches the static shell. For runtime params (not in `generateStaticParams`), any content that depends on the param value (such as `use cache` functions after `await params`) is not included in the prefetch and will be fetched during navigation, showing the Suspense fallback until ready. +> - **Parent Suspense boundaries** (like [`loading.tsx`](/docs/app/api-reference/file-conventions/loading) or a layout Suspense wrapping children) take precedence over page-level `use cache` for runtime params. While build-time params from `generateStaticParams` are still fully cached to disk, runtime params will use the parent fallback UI and stream with memory caching only - no disk files are created. ## Examples From c5d6b7e34eebc3440c740eb0d380e5f66b80bf85 Mon Sep 17 00:00:00 2001 From: Joseph Chamochumbi Date: Wed, 26 Nov 2025 19:51:10 +0100 Subject: [PATCH 11/13] docs: fix flow for gsP html outputs and static shell --- .../03-file-conventions/dynamic-routes.mdx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx index 418960077208d7..9ebc69fb2e8b9c 100644 --- a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx +++ b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx @@ -149,9 +149,12 @@ export default async function Page(props: PageProps<'/[locale]'>) { 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 `` boundaries. -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. +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: -When you use `generateStaticParams`, Next.js also generates a static shell where params are treated as dynamic. When users request params not prerendered at build time, Next.js immediately sends this static shell (containing content like headers, navigation, and layout), while param-dependent content updates the UI when it's ready. This provides a better user experience than waiting for the entire page to render. +- It validates that your route doesn't access dynamic APIs like `cookies()` or `headers()` during prerendering +- It generates static HTML files for both the specific routes you list and a static shell for runtime params + +Prerendered routes load instantly, while runtime params use the static shell to show static content (like headers and navigation) immediately while param-dependent content updates when ready. The sections below show different patterns, from simplest (all runtime) to most optimized (prerendered samples, and caching). We recommend wrapping param access in Suspense boundaries to provide meaningful loading states. From 4f5fc0a76e3d969bd71534123a34888100d022cd Mon Sep 17 00:00:00 2001 From: Joseph Chamochumbi Date: Thu, 27 Nov 2025 15:03:13 +0100 Subject: [PATCH 12/13] docs: gsP and lower scope use cache for ISR --- .../03-file-conventions/dynamic-routes.mdx | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx index 9ebc69fb2e8b9c..c0b90ceb5bbee9 100644 --- a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx +++ b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx @@ -222,17 +222,18 @@ async function Content({ slug }: { slug: Promise }) { } ``` -#### With `generateStaticParams` and `use cache` on page +#### With `generateStaticParams` and `use cache` -Provides immediate UI for prerendered and previously visited routes, with disk persistence similar to ISR. When a runtime param is encountered, Next.js uses the static shell if it has visible content, or prerenders the full page (ISR-style) if the shell is empty. +Provides disk persistence similar to ISR. Prerendered routes are instant, while runtime params wait for the full page to render on first request, then are cached to disk for subsequent visits. -- **Build time**: Validates route, prerenders samples, generates static shell +- **Build time**: Validates route, prerenders samples - **Prerendered params**: Instant - served from disk -- **Runtime params**: Static shell renders immediately (if it has content), then streams param-dependent content. Full page is cached to disk after first visit. +- **Runtime params**: First request waits for full page render, then cached to disk. Subsequent visits are instant. - **Caching**: Disk (persists across server restarts) +Parent Suspense boundaries such as [`loading.tsx`](/docs/app/api-reference/file-conventions/loading) or layout Suspense wrapping children will prevent disk caching for runtime params. + ```tsx filename="app/blog/[slug]/page.tsx" -import { Suspense } from 'react' import { cacheLife } from 'next/cache' export async function generateStaticParams() { @@ -240,29 +241,23 @@ export async function generateStaticParams() { } export default async function Page({ params }: PageProps<'/blog/[slug]'>) { - 'use cache' - cacheLife('days') return (

Blog Post

- Loading...
}> - p.slug)} /> -
+ p.slug)} />
) } async function Content({ slug }: { slug: Promise }) { + 'use cache' + cacheLife('days') const resolvedSlug = await slug + // Fetch your data here return
{/* Your content */}
} ``` -> **Good to know**: -> -> - **Prefetching**: When navigating via ``, Next.js prefetches the static shell. For runtime params (not in `generateStaticParams`), any content that depends on the param value (such as `use cache` functions after `await params`) is not included in the prefetch and will be fetched during navigation, showing the Suspense fallback until ready. -> - **Parent Suspense boundaries** (like [`loading.tsx`](/docs/app/api-reference/file-conventions/loading) or a layout Suspense wrapping children) take precedence over page-level `use cache` for runtime params. While build-time params from `generateStaticParams` are still fully cached to disk, runtime params will use the parent fallback UI and stream with memory caching only - no disk files are created. - ## Examples ### With `generateStaticParams` From 28cee6f9f1979e278cfd7153047e4fb56bd17f0b Mon Sep 17 00:00:00 2001 From: Joseph Chamochumbi Date: Fri, 28 Nov 2025 00:31:05 +0100 Subject: [PATCH 13/13] docs: simplify into isr-like pattern --- .../03-file-conventions/dynamic-routes.mdx | 80 +++++++------------ 1 file changed, 29 insertions(+), 51 deletions(-) diff --git a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx index c0b90ceb5bbee9..b089541f12c12e 100644 --- a/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx +++ b/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.mdx @@ -147,7 +147,7 @@ export default async function Page(props: PageProps<'/[locale]'>) { ### With Cache Components -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 `` boundaries. +When using [Cache Components](/docs/app/getting-started/cache-components) with dynamic routes, params are runtime data that require special handling. During prerendering, param values are unknown (like `/blog/hello-world` or `/blog/123`), so fallback UI using `` boundaries is required when accessing them. 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: @@ -162,12 +162,7 @@ The sections below show different patterns, from simplest (all runtime) to most #### Without `generateStaticParams` -The simplest approach. All params are runtime data. - -- **Build time**: No validation, no prerendering -- **Prerendered params**: None -- **Runtime params**: Reading params requires Suspense fallback UI (build fails without it), shell renders on-demand, UI updates when content resolves -- **Caching**: Memory only (cleared on server restart) +The simplest approach. All params are runtime data. Param access must be wrapped by Suspense fallback UI. Next.js generates a static shell at build time, and content loads on each request. ```tsx filename="app/blog/[slug]/page.tsx" import { Suspense } from 'react' @@ -185,20 +180,26 @@ export default async function Page({ params }: PageProps<'/blog/[slug]'>) { async function Content({ slug }: { slug: Promise }) { const resolvedSlug = await slug - return
{/* Your content */}
+ const res = await fetch(`https://api.vercel.app/blog/${resolvedSlug}`) + const post = await res.json() + return ( +
+

{post.title}

+

{post.content}

+
+ ) } ``` #### With `generateStaticParams` -Enables build-time validation and prerendering of popular routes. Next.js generates both the prerendered param pages and a static shell (where params are treated as dynamic). When a runtime param is encountered, Next.js uses this static shell to render immediately while streaming param-dependent content. +Prerender your most popular or high-value pages by providing params ahead of time. These pages are generated at build time for instant loading. -- **Build time**: Validates route, prerenders samples (e.g., `/1`, `/2`, `/3`), generates static shell -- **Prerendered params**: Instant - served from disk -- **Runtime params**: Static shell renders immediately, UI updates when param-dependent content resolves -- **Caching**: Memory only for runtime params (cleared on server restart) +During the build process, the route is executed with the values returned from `generateStaticParams` to validate that any dynamic content or runtime data access is properly handled with Suspense or `use cache`. -```tsx filename="app/blog/[slug]/page.tsx" +For runtime params, the build process attempts to prerender a static shell treating params as runtime data. To enable disk caching (ISR-style) for runtime params and include param-specific content in the initial HTML, access params outside any Suspense boundaries, including parent boundaries like [`loading.tsx`](/docs/app/api-reference/file-conventions/loading): + +```tsx filename="app/blog/[slug]/page.tsx" highlight={5-7,10,22,24} import { Suspense } from 'react' export async function generateStaticParams() { @@ -206,58 +207,35 @@ export async function generateStaticParams() { } export default async function Page({ params }: PageProps<'/blog/[slug]'>) { + const { slug } = await params + return (

Blog Post

- Loading...
}> - p.slug)} /> -
+ ) } -async function Content({ slug }: { slug: Promise }) { - const resolvedSlug = await slug - return
{/* Your content */}
-} -``` - -#### With `generateStaticParams` and `use cache` - -Provides disk persistence similar to ISR. Prerendered routes are instant, while runtime params wait for the full page to render on first request, then are cached to disk for subsequent visits. - -- **Build time**: Validates route, prerenders samples -- **Prerendered params**: Instant - served from disk -- **Runtime params**: First request waits for full page render, then cached to disk. Subsequent visits are instant. -- **Caching**: Disk (persists across server restarts) - -Parent Suspense boundaries such as [`loading.tsx`](/docs/app/api-reference/file-conventions/loading) or layout Suspense wrapping children will prevent disk caching for runtime params. - -```tsx filename="app/blog/[slug]/page.tsx" -import { cacheLife } from 'next/cache' - -export async function generateStaticParams() { - return [{ slug: '1' }, { slug: '2' }, { slug: '3' }] -} - -export default async function Page({ params }: PageProps<'/blog/[slug]'>) { +async function Content({ slug }: { slug: string }) { + const post = await getPost(slug) return ( -
-

Blog Post

- p.slug)} /> -
+
+

{post.title}

+

{post.content}

+
) } -async function Content({ slug }: { slug: Promise }) { +async function getPost(slug: string) { 'use cache' - cacheLife('days') - const resolvedSlug = await slug - // Fetch your data here - return
{/* Your content */}
+ const res = await fetch(`https://api.vercel.app/blog/${slug}`) + return res.json() } ``` +Build-time validation only covers code paths that execute with the sample params. If your route has conditional logic that accesses runtime APIs (like `cookies()`) for certain param values not in your samples, those branches won't be validated at build time. + ## Examples ### With `generateStaticParams`