ブログ記事や商品ページのように、「URL の一部だけが違う同じ形のページ」をたくさん作りたいことがあります。/blog/1・/blog/2・/blog/hello のためにファイルを1つずつ用意するのは現実的ではありません。Next.js の App Router では、動的ルーティング(Dynamic Routes)を使えば、1つのファイルで無数の URL に対応できます。この記事では、[slug] フォルダの作り方、URL の値を params で受け取る方法、キャッチオール、そして存在しないページの扱いまでを初心者向けに解説します。前提として App Router(app ディレクトリ)を使っていることとします。
目次
[slug] フォルダで動的なパスを作る
App Router では、フォルダ名を角かっこで囲んで [slug] のようにすると、その部分が「任意の値」を受け取る動的なセグメントになります。たとえば app/blog/[slug]/page.tsx というファイルを作ると、/blog/hello や /blog/123 など、/blog/ のあとが何であってもこのページが表示されます。
// /blog/○○ でアクセスされたときに表示されるページ
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params; // URL の [slug] 部分を取り出す
return (
<article>
<h1>記事: {slug}</h1>
<p>このページの slug は「{slug}」です。</p>
</article>
);
}
動的セグメントの値は、ページコンポーネントに渡される params から受け取ります。[slug] というフォルダ名にしたので、params の中身は { slug: '...' } になります。/blog/hello にアクセスすれば slug は 'hello'、/blog/123 なら '123'(文字列)です。なお最近の Next.js では params は Promise になっているため、await params で中身を取り出します。フォルダ名を [id] にすれば params.id、[category] にすれば params.category というように、フォルダ名がそのままプロパティ名になると覚えておきましょう。
受け取った値でデータを取得する
動的ルーティングの本当の目的は、URL の値をもとにその記事や商品のデータを取ってくることです。App Router のページコンポーネントは async にできるので、params から取り出した値を使って直接データを取得できます。
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// id を使って商品データを取得する
const res = await fetch(`https://example.com/api/products/${id}`);
const product = await res.json();
return (
<div>
<h1>{product.name}</h1>
<p>価格: {product.price} 円</p>
</div>
);
}
/products/42 にアクセスすると id が '42' になり、その ID で API からデータを取得しています。1つの page.tsx で、すべての商品ページをまかなえるわけです。fetch はサーバー側で実行されるため、API キーなどを安全に扱える点も App Router の利点です。
複数階層をまとめて受け取る(キャッチオール)
[slug] は1階層ぶんの値だけを受け取ります。/docs/a/b/c のように階層の深さが決まっていないパスをまとめて扱いたいときは、[...slug] という「キャッチオールセグメント」を使います。角かっこの中に ... を付けるのがポイントです。
| フォルダ名 | マッチする例 | params の中身 |
|---|---|---|
[slug] | /docs/intro | { slug: 'intro' } |
[...slug] | /docs/a/b/c | { slug: ['a', 'b', 'c'] } |
[[...slug]] | /docs と /docs/a/b の両方 | 無しのときは {} |
[...slug] では値が文字列の配列になります(/docs/a/b/c なら ['a', 'b', 'c'])。二重の角かっこ [[...slug]] にすると、セグメントが無い /docs 自体にもマッチする「省略可能なキャッチオール」になります。ドキュメントサイトのように階層が自由なパスを1つのページで扱いたいときに便利です。
存在しないページを 404 にする
動的ルーティングは「何でも受け取れる」ため、存在しない ID でもページ自体は表示されてしまいます。データが見つからないときは、notFound() を呼んで 404 ページを表示するのが適切です。
import { notFound } from 'next/navigation';
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const res = await fetch(`https://example.com/api/products/${id}`);
if (!res.ok) {
notFound(); // 見つからなければ 404 ページへ
}
const product = await res.json();
return <h1>{product.name}</h1>;
}
notFound() を呼ぶと、そこで処理が止まり、app 内の not-found.tsx(無ければ Next.js の既定の 404 ページ)が表示されます。存在しない商品 ID にアクセスされたときに、中身のないページを見せずに済みます。動的ルーティングでは「該当データが無いケース」を必ず考えるようにしましょう。
まとめ
Next.js の App Router では、フォルダ名を [slug] のように角かっこで囲むと動的ルーティングになり、1つの page.tsx で多数の URL に対応できます。角かっこ内の名前がそのまま params のプロパティ名になり、const { slug } = await params で URL の値を取り出せます。取り出した値でデータを取得すれば、記事や商品の詳細ページを効率よく作れます。階層が可変のパスは [...slug](配列で受け取る)や、省略可能な [[...slug]] を使います。存在しないデータには notFound() で 404 を返すのを忘れないようにしましょう。まずは [slug] フォルダを1つ作り、params の値を画面に表示するところから試してみてください。