Next.js の App Router で [slug] のような動的ルート(URLの一部が可変になるページ)を作ると、既定ではリクエストのたびにサーバーでページを組み立てます。しかし、ブログ記事のように内容が決まっているページなら、ビルド時にあらかじめHTMLを作っておく方が高速で、サーバーの負荷も下がります。この「事前生成」を動的ルートで行うための関数が generateStaticParams です。この記事では、SSG(Static Site Generation=静的サイト生成)の考え方から、generateStaticParams の基本、実際のデータからパスを作る方法、複数セグメントの扱い、dynamicParams による挙動の制御まで、初心者〜中級者向けに解説します。
目次
なぜ動的ルートを静的生成するのか
SSG(静的サイト生成)とは、ページのHTMLをビルドのタイミングで一度だけ作っておき、あとはその出来上がったファイルを配信する方式のことです。アクセスのたびにサーバーで計算しないため表示が速く、CDN(世界中に配置された配信サーバー)にも載せやすいという利点があります。
問題は、app/blog/[slug]/page.tsx のような動的ルートです。[slug] の部分は hello-world にも nextjs-tips にもなり得るため、Next.js は「どのURLを事前に用意すればよいか」を自動では判断できません。そこで、存在するパスの一覧を Next.js に教える役割を果たすのが generateStaticParams です。この関数が返したパスの分だけ、ビルド時に静的HTMLが生成されます。
generateStaticParams の基本
generateStaticParams は、動的ルートの page.tsx と同じファイルに export します。返すのは、動的セグメントの名前をキーに持つオブジェクトの配列です。[slug] というフォルダなら、キーは slug になります。
// 事前生成したいパスの一覧を返す
export function generateStaticParams() {
return [{ slug: "hello-world" }, { slug: "nextjs-tips" }];
}
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
// params は Promise なので await して取り出す(Next.js 15 以降)
const { slug } = await params;
return <h1>{slug}</h1>;
}
この例では { slug: "hello-world" } と { slug: "nextjs-tips" } の2件を返しているので、ビルド時に /blog/hello-world と /blog/nextjs-tips の2ページが静的HTMLとして生成されます。配列の各要素がそのまま1つのURLに対応する、と考えると分かりやすいです。なお Next.js 15 以降では page が受け取る params が Promise になったため、await で中身を取り出しています。
記事一覧のslugをデータから取得して返す
実際のブログでは、パスをコードに直書きするのではなく、APIやファイルから記事の一覧を取ってきて生成します。generateStaticParams は async にできるので、関数の中で await を使ってデータを取得できます。
type Post = { slug: string; title: string };
export async function generateStaticParams() {
// ビルド時に記事の一覧を取得する
const res = await fetch("https://api.example.com/posts");
const posts: Post[] = await res.json();
// [{ slug: "..." }, ...] の形に変換して返す
return posts.map((post) => ({
slug: post.slug,
}));
}
ポイントは、返す配列の各オブジェクトが { slug: string } の形になっていることです。取得したデータをそのまま返すのではなく、map で セグメント名(ここでは slug)をキーにしたオブジェクトへ変換する点に注意してください。ローカルのMarkdownファイルからslugを集める場合も、最終的にこの形の配列を返せば同じように動きます。
複数の動的セグメントを扱う
app/[category]/[slug]/page.tsx のように、動的セグメントが2つ以上ある場合もあります。このときは、返すオブジェクトにすべてのセグメント名を含めます。category と slug の両方をキーに持つオブジェクトを並べます。
export function generateStaticParams() {
// category と slug の組み合わせごとに1ページ生成される
return [
{ category: "tech", slug: "nextjs" },
{ category: "tech", slug: "typescript" },
{ category: "life", slug: "diary" },
];
}
この例では、/tech/nextjs、/tech/typescript、/life/diary の3ページが生成されます。組み合わせ1つにつき1ページなので、カテゴリーと記事の対応関係をそのまま配列で表現するイメージです。返すオブジェクトに片方のキーが欠けているとエラーになるため、全セグメント分のキーを必ず揃えるようにしましょう。
事前生成していないパスへのアクセスを制御する dynamicParams
generateStaticParams が返さなかったパス、たとえば新しく追加された記事のURLにアクセスがあったとき、どう振る舞うかを決めるのが dynamicParams です。これは page.tsx で boolean の値として export します。既定値は true です。
// 一覧に無いパスは 404 にする
export const dynamicParams = false;
export function generateStaticParams() {
return [{ slug: "hello-world" }];
}
true と false で、事前生成されていないパスへの挙動が次のように変わります。
| dynamicParams | 一覧に無いパスにアクセスしたときの挙動 |
|---|---|
true(既定) | リクエスト時にそのページを生成して表示する。以降はキャッシュされ、次回から静的配信される。 |
false | ページを生成せず、404(Not Found)を返す。 |
記事が固定で、想定外のURLは存在させたくない場合は false にして 404 を返すのが安全です。一方、記事があとから増えるサイトでは、既定の true のままにしておけば、ビルド後に追加されたパスもアクセス時に生成されます。サイトの性質に合わせて選びましょう。
params や generateMetadata との関係
generateStaticParams が返したオブジェクトは、そのまま各ページの params として渡されます。{ slug: "hello-world" } を返せば、そのページの params は { slug: "hello-world" } になる、という対応です。この params は page だけでなく、ページの <title> などを設定する generateMetadata 関数でも同じように受け取れます。
import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
// generateStaticParams が返した slug を受け取れる
const { slug } = await params;
return { title: `記事: ${slug}` };
}
このように、generateStaticParams で「どのパスを作るか」を決め、generateMetadata と page がそれぞれの params を受け取ってページの中身とメタ情報を組み立てる、という流れになります。3つの関数が params を軸につながっていると理解しておくと、動的ルートの静的生成がぐっと分かりやすくなります。
まとめ
generateStaticParams は、[slug] のような動的ルートをビルド時に静的HTMLとして事前生成(SSG)するための関数です。動的ルートの page.tsx で export し、{ slug: "..." } のようにセグメント名をキーにしたオブジェクトの配列を返すと、その件数分のページが生成されます。実際にはAPIやファイルから記事一覧を async で取得し、map でこの形へ変換して返すのが定石です。[category]/[slug] のような複数セグメントでは全セグメント分のキーを揃え、事前生成していないパスへのアクセスは dynamicParams の true/false で挙動を制御します。返した値はそのまま params として page や generateMetadata に渡ります。まずは小さなブログの記事ページで、静的生成を試してみてください。