SyntaxStudy
Sign Up
Next.js Static Site Generation with generateStaticParams
Next.js Beginner 1 min read

Static Site Generation with generateStaticParams

Static Site Generation (SSG) pre-renders pages to HTML at build time. In the App Router, a page is statically generated by default when it does not use any dynamic functions (headers(), cookies(), searchParams) and all its fetch() calls are cached. For dynamic routes like [slug], you export a generateStaticParams function that returns all the parameter values to pre-render. generateStaticParams runs at build time and returns an array of objects, where each object contains the parameter values for one pre-rendered page. It replaces getStaticPaths from the Pages Router and follows the same concept but with a cleaner API. You can also use it at multiple levels of nested dynamic routes, and Next.js will generate pages for all combinations. By default, when a user visits a dynamic route not returned by generateStaticParams, Next.js will generate the page on demand and cache it. This is Incremental Static Regeneration (ISR) at its most basic. You can opt out of this behaviour by setting the dynamicParams export to false, which makes Next.js return a 404 for any path not pre-rendered at build time.
Example
// app/blog/[slug]/page.tsx — Static generation with ISR

import type { Metadata } from 'next';
import { notFound } from 'next/navigation';

interface Post {
  slug: string;
  title: string;
  content: string;
  publishedAt: string;
}

// Tell Next.js which slugs to pre-render at build time
export async function generateStaticParams() {
  const res = await fetch('https://api.example.com/posts?fields=slug');
  const posts: Pick<Post, 'slug'>[] = await res.json();

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

// Revalidate every hour (ISR)
export const revalidate = 3600;

// Allow on-demand generation for slugs not in generateStaticParams
export const dynamicParams = true;

async function getPost(slug: string): Promise<Post | null> {
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { revalidate: 3600, tags: [`post-${slug}`] },
  });
  if (!res.ok) return null;
  return res.json();
}

export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  const post = await getPost(params.slug);
  return { title: post?.title ?? 'Post not found' };
}

export default async function BlogPostPage({
  params,
}: {
  params: { slug: string };
}) {
  const post = await getPost(params.slug);
  if (!post) notFound();

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