Next.js generateStaticParams: Dynamic Routes, ISR, and Static Generation (2026)

How to use Next.js generateStaticParams for dynamic routes, ISR, dynamicParams, and Partial Prerendering in Next.js 15 and 16, with copy-pasteable examples.

Next.js generateStaticParams Guide

Updated: June 7, 2026

generateStaticParams is the Next.js App Router function that tells the framework which dynamic route segments to render at build time, replacing the legacy getStaticPaths from the Pages Router. You export it from a page.tsx living under a dynamic segment (like [slug]), return an array of param objects, and Next.js pre-renders one HTML file per entry. In Next.js 15 and 16, it also interacts with dynamicParams, the cache lifecycle, and Partial Prerendering. Getting these pieces right is the difference between a fast static site and an accidentally dynamic one.

  • generateStaticParams runs once at build time and returns an array of param objects that Next.js uses to pre-render dynamic routes like /blog/[slug].
  • It replaces the Pages Router's getStaticPaths. There is no fallback field. Use the separate dynamicParams export instead.
  • Setting export const dynamicParams = false returns 404 for any unknown slug, while the default true generates pages on first request and caches them.
  • Combine it with export const revalidate = 3600 to get Incremental Static Regeneration (ISR) for content that changes between deploys.
  • In Next.js 16, generateStaticParams composes with "use cache" and Partial Prerendering. You can statically generate the shell and stream dynamic chunks.
  • Returning an empty array silently moves your site from static to dynamic. It's a common production footgun.

What is generateStaticParams?

generateStaticParams is an async function you export from a page or layout file inside a dynamic route folder. It returns an array of objects, each one describing a concrete value of the dynamic segment. When Next.js builds your site, it calls this function once, walks the returned array, and renders a static HTML file for each entry. The result is a fully pre-rendered site for known content, served from the edge with zero compute on request.

In the App Router, every dynamic segment defined with square brackets (think [slug], [id], [...rest]) needs a way to know which values exist. Without generateStaticParams, the segment is treated as fully dynamic and rendered on demand on every request. That's occasionally what you want for personalised dashboards, but for blog posts, product pages, documentation, and marketing landers, you almost always want static generation. The function is the bridge between your data source (a CMS, a database, a markdown folder) and Next.js's static rendering pipeline.

One subtle thing: the function name is generateStaticParams, not generateStaticPaths. The arguments and return shape changed from the Pages Router, and the new API is intentionally simpler. No paths wrapper, no fallback field, just an array of plain param objects. It also runs in the same module graph as your page, which means you can share helpers and types between the param generator and the component itself without the awkward duplication the Pages Router required.

A basic dynamic route example

Here's the smallest useful pattern. The file lives at app/blog/[slug]/page.tsx and assumes you have a function that lists all blog post slugs from your data source.

// app/blog/[slug]/page.tsx
import { getAllPostSlugs, getPostBySlug } from '@/lib/posts';
import { notFound } from 'next/navigation';

export async function generateStaticParams() {
  const slugs = await getAllPostSlugs();
  return slugs.map((slug) => ({ slug }));
}

// In Next.js 15+, params is a Promise. Await it before reading.
export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPostBySlug(slug);
  if (!post) notFound();

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.html }} />
    </article>
  );
}

Two things to notice. First, the returned objects must use the exact segment name as the key. { slug } matches the [slug] folder. If you mistype it as { id } for a [slug] folder, Next.js silently skips the entry and your pages won't pre-render. (I lost an afternoon to this one in my last project. The build was green, the route table was empty.) Second, since Next.js 15, params arrives as a Promise, so you must await it. If you're still upgrading, our guide on the Next.js 15 params Promise migration covers every gotcha including the codemod's blind spots.

generateStaticParams vs getStaticPaths

Coming from the Pages Router? Here's exactly what changed between the two APIs.

FeaturegetStaticPaths (Pages)generateStaticParams (App)
Return shape{ paths: [...], fallback }Plain array of param objects
Fallback controlfallback: false | true | 'blocking'Separate dynamicParams export
Path format{ params: { slug: 'a' } }{ slug: 'a' } (no wrapper)
Co-located withgetStaticPropsThe page component itself
Layouts supportNoYes, can be exported from layouts
Type of segmentPages onlyPages and layouts
Runs inNode.js onlyNode.js (Edge unsupported)

The migration is usually mechanical. Unwrap paths, drop the params wrapper from each entry, delete the fallback field, and add a dynamicParams export at the top level of the file if you need anything other than the default true behavior. The full migration playbook is in the official Next.js generateStaticParams API reference.

dynamicParams and fallback behavior

The fallback option from getStaticPaths moved to a separate file-level export called dynamicParams. It's a boolean, and it lives next to generateStaticParams in the same page.tsx.

// app/blog/[slug]/page.tsx

// Default. Unknown slugs render on first request and are cached.
export const dynamicParams = true;

// Strict. Unknown slugs return 404. Equivalent to fallback: false.
export const dynamicParams = false;

There's no equivalent to fallback: true with its loading state. The App Router solves that case with React Suspense and loading.tsx boundaries instead. For a refresher on streaming UI patterns, see our guide on Next.js Streaming and Suspense.

Choose dynamicParams = false for finite, fully-known content sets, like a docs site whose URL list is fixed at build. Choose the default true for content that grows between deploys: new blog posts published from a CMS, freshly added products, user-generated profiles. With the default, the first request for an unknown slug renders dynamically, caches the result, and serves subsequent requests statically. Combined with on-demand revalidation, this gives you the performance of SSG with the freshness of SSR.

Catch-all and nested dynamic segments

Dynamic routes can include multiple segments (think app/[category]/[product]/page.tsx) or catch-all segments like app/docs/[...slug]/page.tsx. generateStaticParams handles both, but the return shape changes.

// app/[category]/[product]/page.tsx (nested segments)
export async function generateStaticParams() {
  const products = await db.product.findMany({
    select: { categorySlug: true, slug: true },
  });
  return products.map((p) => ({
    category: p.categorySlug,
    product: p.slug,
  }));
}

// app/docs/[...slug]/page.tsx (catch-all)
export async function generateStaticParams() {
  const pages = await getAllDocPages();
  return pages.map((p) => ({
    slug: p.path.split('/'), // ['guides', 'intro'] for /docs/guides/intro
  }));
}

For catch-all routes the value must be an array of strings, one per URL segment. A common bug is returning the joined path as a single string. Next.js then generates a route with a literal slash in the segment, which won't match incoming requests. For optional catch-all segments ([[...slug]]), you can include an entry with an empty array { slug: [] } to pre-render the bare parent route.

When you have a layout above a dynamic segment that also needs to know the params (for breadcrumbs, navigation, or og-image generation), export generateStaticParams from the layout too. Next.js merges the params from layout and page, building the cartesian product of param sets. In practice, you should still co-locate the canonical source-of-truth in the deepest segment to avoid drift between layout and page.

Combining with ISR and revalidate

Static generation is great until content changes. The App Router gives you two complementary tools: time-based revalidation and on-demand revalidation. Both layer on top of generateStaticParams without any special configuration.

// app/blog/[slug]/page.tsx
// Re-render any individual page at most once per hour.
export const revalidate = 3600;

export async function generateStaticParams() {
  // Generate only the most recent 50 posts at build time.
  // Older posts render on first request and are cached forever
  // (since dynamicParams defaults to true).
  const recent = await getRecentPostSlugs(50);
  return recent.map((slug) => ({ slug }));
}

This pattern (pre-render the hot set, lazy-generate the long tail) is the sweet spot for blogs, e-commerce, and documentation. Build times stay short. The first visitor to an old post pays a one-time render cost, then every visitor after gets a static response. For instant invalidation after a CMS edit, call revalidatePath or revalidateTag from a webhook. Our deeper dive on why Next.js caches don't invalidate after server actions explains the cache-tag semantics in Next.js 15 and 16.

Using it with a headless CMS

The real-world pattern almost always involves a headless CMS (Contentful, Sanity, Payload, Strapi, or a custom API). The shape stays identical, but you'll want to batch and paginate the fetch so you don't choke the CMS during a build.

// app/blog/[slug]/page.tsx with a Sanity-like CMS
import { sanityClient } from '@/lib/sanity';

export async function generateStaticParams() {
  // Fetch only what you need. Never the full post body.
  const posts: { slug: string }[] = await sanityClient.fetch(
    `*[_type == "post" && defined(slug.current)][]{
       "slug": slug.current
     }`
  );

  return posts.map((post) => ({ slug: post.slug }));
}

export const revalidate = 60; // ISR window for content freshness

A few production rules of thumb. Always fetch the smallest possible projection (slug only, not the full document). Cache the result with React's cache() so you don't pay twice when the layout also calls generateStaticParams. Add a fallback for empty arrays during preview deployments where your CMS dataset may be empty. Otherwise the build will succeed with zero generated pages and you'll wonder why nothing is live. Finally, in CI, run a smoke test that asserts generateStaticParams returned more than zero entries. It catches outages on the CMS side before they ship. I hit this exact bug shipping a 200-page docs site last quarter: the CMS API token rotated mid-build, the fetch silently returned an empty array, and the deploy nuked every URL.

Partial Prerendering in Next.js 16

Next.js 16 stabilises Partial Prerendering (PPR) and the "use cache" directive, and both work alongside generateStaticParams rather than replacing it. The pattern: use generateStaticParams to pre-render the shell of each dynamic route, then mark dynamic chunks (user-specific data, real-time prices) as streamed-on-demand. The HTML still ships from the CDN, and only the dynamic holes round-trip to the origin.

// app/products/[id]/page.tsx with PPR + use cache
import { Suspense } from 'react';
import { LivePrice } from '@/components/live-price';

export const experimental_ppr = true;

export async function generateStaticParams() {
  const ids = await db.product.findMany({ select: { id: true } });
  return ids.map(({ id }) => ({ id: String(id) }));
}

export default async function ProductPage({
  params,
}: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const product = await getProduct(id); // cached, part of static shell

  return (
    <article>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <Suspense fallback={<span>Loading price…</span>}>
        <LivePrice productId={id} />
      </Suspense>
    </article>
  );
}

For the full mental model on caching directives, see our walkthrough of Next.js 16 Cache Components and Partial Prerendering. The official Next.js 16 release notes describe the stability guarantees in detail.

Why is generateStaticParams not working?

So, if the function runs but no pages are generated, walk through this checklist. Each item is a real production bug I've debugged more than once.

  1. Key mismatch. The object key in each returned entry must equal the folder name. [slug] requires { slug }, not { id }. Next.js silently drops mismatched entries.
  2. Values must be strings. Numbers, UUID objects, and booleans are not allowed. Coerce with String(id).
  3. Empty array. If generateStaticParams returns [], no pages pre-render. Check that your data source is reachable from the build environment. Your build runner often can't see staging-only databases or VPN-gated services.
  4. Forced dynamic. If the page or any parent layout sets export const dynamic = 'force-dynamic', generateStaticParams is ignored entirely. The same applies to calling cookies(), headers(), or searchParams at the page top level without a Suspense boundary.
  5. Edge runtime. Pages with export const runtime = 'edge' cannot be statically generated via generateStaticParams. Switch to nodejs or render dynamically.
  6. Cache directives conflict. Using fetch(..., { cache: 'no-store' }) inside the page body opts the route out of static rendering, even if generateStaticParams runs.

Performance and build-time tradeoffs

Honestly, the biggest generateStaticParams mistake at scale is returning everything. For a site with 200,000 products, a full static build will take 30+ minutes and produce a massive .next directory that may not fit in your CI cache. The fix is almost never "use ISR for everything." It's "pre-render the hot 5%, lazy-render the rest."

A few production tactics that work well. First, sort your slugs by traffic and pre-render only the top N (we typically pick the top 10 to 20 percent by hit rate from the last 30 days of analytics). Second, parallelise your fetch by category or shard. Next.js calls generateStaticParams once, but you can issue parallel queries inside. Third, set a longer revalidate window for low-change pages and rely on on-demand revalidatePath webhooks for instant CMS edits. Fourth, audit which routes are accidentally dynamic. Run next build and look for the ƒ (dynamic) marker in the route table where you expected (static).

Frequently Asked Questions

Does generateStaticParams run at build time or runtime?

It runs at build time by default. Next.js invokes it once during next build to enumerate the pages to pre-render. With dynamicParams = true (the default), unknown segments are also rendered on the first runtime request and cached. It never runs on subsequent visits to an already-cached page.

Can I use generateStaticParams with the Edge runtime?

No. The function requires the Node.js runtime because the build worker needs full filesystem and database access. Pages exporting runtime = 'edge' can't be statically generated this way. Render them dynamically, or move just the dynamic data fetching to the edge while keeping the page in Node.

What happens if generateStaticParams returns an empty array?

No pages are pre-rendered at build time. If dynamicParams is true (the default), the route still works and every request renders on demand and is cached. If dynamicParams is false, every URL under that segment returns 404.

Is generateStaticParams the same as getStaticPaths?

Functionally similar, but the API is different. generateStaticParams returns a flat array of param objects instead of { paths, fallback }, and the fallback behavior moved to a separate dynamicParams export. It also works in layouts, not just pages, which getStaticPaths never supported.

How do I revalidate a single page generated by generateStaticParams?

Call revalidatePath('/blog/' + slug) from a Server Action or Route Handler, typically triggered by a CMS webhook. For tag-based invalidation, attach a tag to your fetch calls and call revalidateTag('post'). Both work whether the page was generated at build time or on-demand.

Editorial Team
About the Author Editorial Team

Our team of expert writers and editors.