ウェブエンジニア問題集

動的ルーティング — [slug]・paramsとgenerateStaticParamsでページを量産する

ブログ記事、商品ページ、ユーザープロフィール——「URLの一部だけが違う、同じ形のページ」は無数にあります。これらを1ページずつ手で作るのは現実的ではありません。Next.jsの動的ルーティングを使えば、1つのファイルで何百・何千ページ分をまかなえます。

この章では、[slug] という動的セグメントの作り方、params の受け取り方、そして静的生成(SSG)につなぐ generateStaticParams までを押さえます。

学習者学習者

記事が100本あったら、page.tsx を100個作るの…?さすがに無理だよね。

動的セグメント — フォルダ名を [ ] で囲む

URLの一部を変数として扱いたいときは、フォルダ名を角かっこ [ ] で囲みます。これを動的セグメントと呼びます。

app/
└── blog/
    └── [slug]/
        └── page.tsx      → /blog/任意の文字列

この1ファイルで、/blog/hello/blog/nextjs-tips/blog/2026-summary などすべてに対応できます。[slug] の部分(hello など)は params 経由で受け取れます。

1つのテンプレートから多数のページが生まれるイメージ

params の受け取り方(Next.js 15以降)

動的セグメントの値は、ページコンポーネントの params から取り出します。**Next.js 15以降、params は Promise(非同期)**になったため、await して受け取ります。

props の構造:

プロパティ説明
paramsPromise<{ [key]: string }>動的セグメントの値。[slug] なら { slug: "..." }
searchParamsPromise<{ [key]: string }>?key=value のクエリ文字列(詳細は第7章
// src/app/blog/[slug]/page.tsx
export default async function BlogPostPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params; // ← await で取り出す
 
  return <h1>記事: {slug}</h1>;
}
先生先生

フォルダ名 [slug] の「slug」が、そのまま params.slug のキー名になる。[id] なら params.id[category] なら params.category だよ。

実例 — IDから記事を取得して表示

params で受け取ったIDを使って、データを取得するのが典型的な流れです。データ取得(fetch)は第7章で詳しく扱います。

// src/app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
 
export default async function BlogPostPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const res = await fetch(`https://api.example.com/posts/${slug}`);
 
  if (!res.ok) {
    notFound(); // 記事が無ければ404へ(第8章で解説)
  }
 
  const post = await res.json();
 
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  );
}

generateStaticParams — ビルド時にページを静的生成する

動的ルーティングは、デフォルトではアクセスのたびにサーバーでページを生成します。しかしブログ記事のように内容が頻繁に変わらないページは、ビルド時にあらかじめHTMLを作っておく(静的生成・SSG)方が高速で、SEOにも有利です。

そのために使うのが generateStaticParams です。「どんな params の組み合わせを事前に生成すべきか」を返します。

戻り値: params オブジェクトの配列(各要素が1ページに対応)

// src/app/blog/[slug]/page.tsx
 
// ビルド時に「どのslugを生成するか」を返す
export async function generateStaticParams() {
  const posts = await fetch("https://api.example.com/posts").then((r) => r.json());
 
  return posts.map((post: { slug: string }) => ({
    slug: post.slug, // { slug: "hello" }, { slug: "nextjs-tips" }, ...
  }));
}
 
export default async function BlogPostPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  // ...
}

これで、ビルド時に全記事のHTMLが生成され、/blog/hello などへ即座に応答できるようになります。

catch-all セグメント — 複数階層をまとめて受け取る

[slug] は1階層分ですが、/docs/a/b/c のように階層数が決まっていないURLを1ファイルで受けたいこともあります。その場合は [...slug](catch-all)を使います。

書き方マッチするURLparams
[slug]/docs/a{ slug: "a" }
[...slug]/docs/a/b/c{ slug: ["a", "b", "c"] }(配列)
[[...slug]]/docs/docs/a/b省略時は空。任意の階層
// src/app/docs/[...slug]/page.tsx
export default async function DocsPage({
  params,
}: {
  params: Promise<{ slug: string[] }>;
}) {
  const { slug } = await params; // ["guide", "intro"] など配列で届く
  return <p>パス: {slug.join(" / ")}</p>;
}

まとめ

  • フォルダ名を [slug] のように [ ] で囲むと動的セグメントになり、1ファイルで多数のURLに対応できる
  • 値は params から取り出す。Next.js 15以降は await params(Promise)
  • フォルダ名([id]id)がそのまま params のキー名になる
  • generateStaticParams で、ビルド時にどのページを静的生成するかを指定できる(SSG)
  • 階層数が不定のURLは [...slug](catch-all)で配列として受け取る

次の章では、ここで何度か登場した データ取得とキャッシュ を本格的に扱います。fetch の結果をどうキャッシュし、いつ再取得するか——Next.jsならではの仕組みを理解しましょう。