generateStaticParams
List the param values of a dynamic segment so Next.js prerenders those routes at build time.
export async function generateStaticParams(options?: {
params: Record<string, string | string[]>
}): Promise<Array<Record<string, string | string[]>>>- Added in Next.js v13.0.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$options.paramsoptional | Record<string, string | string[]> | The params generated by a parent segment's generateStaticParams. Present only when an ancestor also exports the function; the child runs once per parent param set. Read synchronously — this argument is not a Promise. |
Return value
Array<Record<string, string | string[]>> | Promise<Array<Record<string, string | string[]>>>An array of objects, each describing one route to prerender. Keys are segment names and values are what to fill them with: { id: string }[] for /product/[id], { category: string, product: string }[] for /products/[category]/[product], and { slug: string[] }[] for a catch-all /products/[...slug].
Errors
With Cache Components enabled, returning an empty array is a build error; without it, failing to return an array at all makes the route render dynamically instead of raising.
Changelog
- PHP v15.0.0The params prop of the page or layout itself is a Promise and must be awaited; the params argument passed into generateStaticParams stays synchronous.
- PHP v13.0.0Introduced as the App Router replacement for the Pages Router getStaticPaths.
Export this from a page, layout or route file inside a dynamic segment to turn on-demand rendering into build-time HTML. It is the App Router's answer to getStaticPaths, and it works with pages, layouts and Route Handlers alike.
When to reach for it
Whenever the set of routes is knowable at build time and small enough to be worth prerendering — a blog index, a docs tree, a product catalogue. Pair it with generateMetadata: fetch is memoized across both, so listing the slugs and reading each record costs one request per URL, not two.
What trips people up
- A segment can generate params for itself and for ancestor segments, never for descendants.
[category]/layout.jscannot produce[product]. - Returning a partial list is fine — the rest render on first visit. Set
dynamicParams = falseif unlisted paths should 404 instead. - You must always return an array. Returning
undefinedquietly makes the whole route dynamic. - It runs before the layouts and pages during
next build, once per navigation duringnext dev, and not at all during revalidation.
Example
import { notFound } from 'next/navigation'
type Post = { slug: string; title: string; body: string }
export async function generateStaticParams() {
const posts: Post[] = await fetch('https://api.example.com/posts').then(
(res) => res.json()
)
return posts.map((post) => ({
slug: post.slug,
}))
}
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const res = await fetch(`https://api.example.com/posts/${slug}`)
if (!res.ok) notFound()
const post: Post = await res.json()
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
)
}When to use it
- Prerender every blog post from a CMS listing at build time.
- Build only the 100 most popular products and render the long tail on first request.
- Lock a marketing route to a fixed set of slugs so anything else 404s.
- Statically generate JSON responses from a Route Handler for a fixed set of ids.
Frequently asked questions
What replaced getStaticPaths in the App Router?
generateStaticParams replaces getStaticPaths. It returns a flat array of param objects rather than { paths, fallback }, and the fallback behaviour moved to the dynamicParams route segment config: dynamicParams = true (the default) renders unlisted paths on demand, dynamicParams = false makes them 404.
Why is my dynamic route still rendering on every request?
Most often the function returned something other than an array — undefined from a missing return, or a rejected promise swallowed upstream — which leaves the route dynamic. It can also be that the page reads cookies(), headers() or searchParams, any of which opts the route into request-time rendering no matter what params were generated.
Does generateStaticParams run again on revalidation?
No. It runs during next build, and during next dev when you navigate to the route, but ISR revalidation re-renders existing paths without calling it again. New paths appear only through dynamicParams rendering them on first visit, or through a new build.
More examples
Prerender a subset, 404 the rest
The build emits exactly ten HTML files. With dynamicParams left at its default of true, an eleventh slug would be rendered on its first request and then cached; with it set to false the same request returns a 404 without ever invoking the page. Dropping the .slice(0, 10) and keeping dynamicParams = false is the pattern for a fully closed route set.
// app/blog/[slug]/page.tsx
// Anything not returned below becomes a 404 instead of
// being rendered on demand.
export const dynamicParams = false
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then((res) =>
res.json()
)
// Render the ten most recent posts at build time
return posts.slice(0, 10).map((post: { slug: string }) => ({
slug: post.slug,
}))
}
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
return <h1>{slug}</h1>
}Two dynamic segments, generated top down
The layout runs first and yields one entry per category; the page's function then runs once per category, receiving { category } synchronously — it is not a Promise here, unlike the page's own params prop. The page only returns { product }, because [category] was already supplied by the ancestor. The build multiplies the two levels together into one route per category/product pair.
// app/products/[category]/layout.tsx
export async function generateStaticParams() {
const categories = await fetch('https://api.example.com/categories').then(
(res) => res.json()
)
return categories.map((category: { slug: string }) => ({
category: category.slug,
}))
}
export default function Layout({ children }: { children: React.ReactNode }) {
return <section>{children}</section>
}
// app/products/[category]/[product]/page.tsx
export async function generateStaticParams({
params: { category },
}: {
params: { category: string }
}) {
const products = await fetch(
`https://api.example.com/products?category=${category}`
).then((res) => res.json())
return products.map((product: { id: string }) => ({
product: product.id,
}))
}
export default async function Page({
params,
}: {
params: Promise<{ category: string; product: string }>
}) {
const { category, product } = await params
return (
<h1>
{category}/{product}
</h1>
)
}Static API responses from a Route Handler
next build writes three static JSON responses, so /api/posts/1 is served from disk rather than invoking the handler. The array is hard-coded here because the ids are fixed; returning [] instead prerenders no ids at build time but still statically generates each path the first time it is visited — it is omitting the return altogether, so that the function yields something other than an array, that leaves the handler dynamically rendered. Note that the handler's params is a Promise, while the array returned by generateStaticParams is plain data.
// app/api/posts/[id]/route.ts
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
return Response.json({ id, title: `Post ${id}` })
}
Discussion