generateStaticParams

List the param values of a dynamic segment so Next.js prerenders those routes at build time.

Signature
export async function generateStaticParams(options?: {
  params: Record<string, string | string[]>
}): Promise<Array<Record<string, string | string[]>>>
  • Added in Next.js v13.0.0

Parameters

ParameterTypeDescription
$options.paramsoptionalRecord<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.js cannot produce [product].
  • Returning a partial list is fine — the rest render on first visit. Set dynamicParams = false if unlisted paths should 404 instead.
  • You must always return an array. Returning undefined quietly makes the whole route dynamic.
  • It runs before the layouts and pages during next build, once per navigation during next dev, and not at all during revalidation.

Example

Example · typescript
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.

Example · typescript
// 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.

Example · typescript
// 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.

Example · typescript
// 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

  • Be the first to comment on this lesson.