generateStaticParams で全記事を事前ビルド — Next.js 静的サイト生成の仕組み

Next.js で posts/[slug] のような動的ルートを静的ファイルとして書き出すには、generateStaticParams で生成対象のパスを列挙する必要があります。この記事では、ブログの記事ページを例に取りながら、この関数の実装パターンと output: "export" 時の挙動を整理します。

なぜ generateStaticParams が必要か

Next.js の動的ルートセグメント([slug] など)は、ビルド時点で「どの値が存在するか」をフレームワークが知ることができません。静的エクスポートをする際には generateStaticParams で全パターンを列挙し、対応する HTML を事前に生成しておく必要があります。

posts/
  hello-world.md   →  /posts/hello-world
  next-tips.md     →  /posts/next-tips

この例では hello-worldnext-tips という 2 つのスラッグを、ビルド時に Next.js へ伝える必要があります。

基本的な実装

// src/app/posts/[slug]/page.tsx

import { getPostData, getAllSlugs } from "@/app/lib/functions";

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

generateStaticParams{ slug: string }[] の配列を返します。slug の名前は動的セグメントのフォルダ名([slug])と一致させる必要があります。

getAllSlugs の実装

// src/app/lib/functions.ts

import fs from "fs";
import path from "path";

const postsDirectory = path.join(process.cwd(), "posts");

export function getAllSlugs(): string[] {
  const filenames = fs.readdirSync(postsDirectory);
  return filenames
    .filter((name) => name.endsWith(".md"))
    .map((name) => name.replace(/\.md$/, ""));
}

ファイル名と URL スラッグを 1:1 対応させるシンプルな設計です。

ページコンポーネントとの連携

generateStaticParams が返した各スラッグは、ページコンポーネントの params プロパティで受け取れます。

Next.js 15 以降では paramsPromise 型に変更されたため、await して使う必要があります。

// src/app/posts/[slug]/page.tsx(Next.js 15 以降)

type Props = {
  params: Promise<{ slug: string }>;
};

export default async function PostPage({ params }: Props) {
  const { slug } = await params; // Next.js 15 以降は await が必要
  const post = await getPostData(slug);

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

Next.js 14 以前では params は同期的に受け取れました。generateStaticParams 関数自体の書き方はバージョン間で変わりません。

output: “export” との関係

next.config.jsoutput: "export" を設定すると、npm run build 実行時にすべてのページが HTML ファイルとして書き出されます。

// next.config.js
const nextConfig = {
  output: "export",
};

このとき、動的ルートに対して generateStaticParams が定義されていないとビルドが失敗します。

Error: Page "/posts/[slug]" is missing "generateStaticParams()" ...

dynamicParams で未知のパスをコントロールする

サーバーあり構成では dynamicParams 設定で未定義パスの挙動を制御できます。

// generateStaticParams に含まれないパスを 404 にする(Pages Router の fallback: false 相当)
export const dynamicParams = false;

複数のセグメントを持つルート

タグページのように [tag]/[page] など複数のセグメントがある場合は、すべての組み合わせを列挙します。

// src/app/tags/[tag]/[page]/page.tsx

export async function generateStaticParams() {
  const posts = await getPostData();
  const allTags = [...new Set(posts.flatMap((p) => p.tags))];

  const params: { tag: string; page: string }[] = [];

  for (const tag of allTags) {
    const filtered = posts.filter((p) => p.tags.includes(tag));
    const totalPages = Math.ceil(filtered.length / POSTS_PER_PAGE);

    for (let i = 1; i <= totalPages; i++) {
      params.push({ tag, page: String(i) });
    }
  }

  return params;
}

業務での使いどころ

generateStaticParams が真価を発揮するのは、外部データを元にページを生成したいときです。

Headless CMS からパスを生成する例:

export async function generateStaticParams() {
  const res = await fetch("https://api.example.com/posts?fields=slug");
  const posts: { slug: string }[] = await res.json();

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

Contentful や microCMS などと組み合わせると、CMS 上で記事を管理しながら静的ファイルとして配信するアーキテクチャが実現できます。

DB の ID を使う例:

export async function generateStaticParams() {
  const db = await getDatabase();
  const articles = await db.select("id").from("articles").where("published", true);

  return articles.map(({ id }) => ({ id: String(id) }));
}

公開済みの記事のみパスを生成し、非公開記事の HTML は出力しない制御もシンプルに書けます。

ハマりやすいポイント

セグメント名の不一致

返すオブジェクトのキーとフォルダ名のセグメント名が一致しないとエラーになります。

フォルダ: app/posts/[postId]/page.tsx
返すキー: { postId: "..." }  ← slug ではなく postId に合わせる

数値スラッグは文字列に変換する

// NG: { page: 1 }
// OK: { page: "1" }
params.push({ page: String(i) });

ファイル名に含まれる拡張子を除去し忘れる

.map((name) => name.replace(/\.md$/, ""))

generateStaticParams は Server Component 専用

クライアントコンポーネント("use client" を持つファイル)に書いても動作しません。page.tsx に定義するのが基本です。

Pages Router の getStaticPaths との比較

項目Pages RouterApp Router
関数名getStaticPathsgenerateStaticParams
戻り値{ paths: [...], fallback: false }[{ slug: "..." }]
fallback の指定必要不要
配置場所pages/posts/[slug].tsxapp/posts/[slug]/page.tsx

App Router 版の方が戻り値がシンプルで、fallback の概念がないぶん記述量が減っています。

まとめ

  • generateStaticParams は動的ルートの全パスをビルド時に列挙する関数
  • output: "export" を使う場合は定義が必須。なければビルドが失敗する
  • 返すオブジェクトのキーはフォルダの [セグメント名] と一致させる
  • 数値は必ず String() で文字列に変換する
  • Headless CMS や DB と組み合わせると、外部データを静的ファイルとして配信できる