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-world と next-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 以降では params が Promise 型に変更されたため、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.js で output: "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 Router | App Router |
|---|---|---|
| 関数名 | getStaticPaths | generateStaticParams |
| 戻り値 | { paths: [...], fallback: false } | [{ slug: "..." }] |
fallback の指定 | 必要 | 不要 |
| 配置場所 | pages/posts/[slug].tsx | app/posts/[slug]/page.tsx |
App Router 版の方が戻り値がシンプルで、fallback の概念がないぶん記述量が減っています。
まとめ
generateStaticParamsは動的ルートの全パスをビルド時に列挙する関数output: "export"を使う場合は定義が必須。なければビルドが失敗する- 返すオブジェクトのキーはフォルダの
[セグメント名]と一致させる - 数値は必ず
String()で文字列に変換する - Headless CMS や DB と組み合わせると、外部データを静的ファイルとして配信できる