◀ 2.

Next.js App Router のルーティング入門

▶

この記事の要点

  • フォルダが URL のセグメントになり、page.tsx を置いた時点でそのパスが公開される。フォルダを作っただけでは URL にならない。
  • 角かっこで囲んだフォルダ名が動的セグメント。[slug] が 1 階層、[...slug] が全階層、[[...slug]] は省略可能。
  • バージョン 16 では params と searchParams が Promise。必ず await してから中身を取り出す。
  • 丸かっこで囲んだ (group) は URL に出ない整理用フォルダ。アンダースコア始まりの _folder はルーティング対象から外れる。
  • ページ間の移動は next/link の Link。先読みとクライアント遷移が効く。

App Router のルーティングは「フォルダの形がそのまま URL の形になる」という単純な規則で成り立っています。ルーティング設定ファイルは存在せず、どんな URL が生えているかはディレクトリツリーを見れば分かります。この記事では、その対応関係と、実務でよく使う動的セグメント・整理用フォルダまでを押さえます。

1フォルダが URL になる

基本の規則は 2 つだけです。

  • フォルダが URL のセグメント(/ で区切られた 1 区画)を定義する
  • page.tsx を置いたフォルダだけが、外部からアクセスできる URL になる

裏を返すと、フォルダを作っただけでは URL は生えません。これは意図的な設計で、コンポーネントやユーティリティを route と同じ場所に置いても、うっかり公開されてしまう心配がないようになっています。

ファイルの場所対応する URL
app/page.tsx/
app/blog/page.tsx/blog
app/blog/authors/page.tsx/blog/authors
app/blog/utils.ts(URL にならない。同じ場所に置いても安全)

ページの実体は、React コンポーネントを default でエクスポートするだけです。

// app/blog/page.tsx
export default function Page() {
  return <h1>ブログ一覧</h1>
}

2動的セグメントで URL に値を埋める

記事 ID や商品コードのように値が可変な URL は、フォルダ名を角かっこで囲んで表現します。これを動的セグメントと呼びます。

フォルダマッチする URL用途
app/blog/[slug]/page.tsx/blog/hello1 階層ぶんの値
app/shop/[...slug]/page.tsx/shop/clothing、/shop/clothing/shirts階層の深さが決まらないカテゴリなど
app/docs/[[...slug]]/page.tsx/docs と、その配下すべて省略も許したいドキュメント階層

受け取った値は params という props に入ります。ここがバージョン 16 での最大の変更点で、params は Promise になりました。同期的に params.slug と書くとエラーになります。

// app/blog/[slug]/page.tsx
export default async function BlogPostPage({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params        // 必ず await する
  const post = await getPost(slug)

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  )
}

TypeScript を使っている場合は、型を手で書く代わりにグローバルヘルパーの PageProps が使えます。この型は next dev や next build の実行時に自動生成され、インポートは不要です。

// app/blog/[slug]/page.tsx
export default async function Page(props: PageProps<'/blog/[slug]'>) {
  const { slug } = await props.params
  return <h1>記事: {slug}</h1>
}

3クエリ文字列を読む

?page=2 のようなクエリは searchParams で受け取ります。これも Promise です。

export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
  const { page } = await searchParams
  // page を使ってデータを取りに行く
}

クエリはリクエストが届いて初めて確定する値なので、これを読むとそのページはリクエスト時に描画されることになります。使い分けの目安は次のとおりです。

searchParams を使う
ページャや絞り込みのように、クエリの値を使ってサーバー側でデータを取りに行くとき。
useSearchParams を使う
すでに読み込み済みの一覧をブラウザ側だけで絞り込むなど、クライアントで完結するとき。

4URL に出ないフォルダ

アプリが育つと「URL は変えたくないが、コードはグループごとに分けたい」という要求が出てきます。これに応えるのが 2 種類の特別なフォルダ名です。

app/ (marketing)/丸かっこ → URL に出ない about/page.tsx→ /about (shop)/ cart/page.tsx→ /cart layout.tsx→ (shop) 配下だけの共通レイアウト blog/ _components/Card.tsxアンダースコア → ルーティング対象外
(group) は URL を変えずにレイアウトを分けるため、_folder は確実にルーティングから外すために使う。

ルートグループ (group) はフォルダ名を丸かっこで囲む書き方です。URL には現れないので、app/(marketing)/about/page.tsx は /about になります。グループごとに layout.tsx を置けるため、「公開ページと管理画面でヘッダーを変えたいが、URL の階層は増やしたくない」という場面で使います。

プライベートフォルダ _folder はアンダースコアで始めるフォルダで、そのフォルダと配下すべてがルーティングから除外されます。前述のとおり同居ファイルは元々 URL になりませんが、意図を明示したいときや、将来 Next.js が追加する予約ファイル名と衝突させたくないときに有効です。

5ページ間を移動する

画面遷移には next/link の Link コンポーネントを使います。見た目は a タグですが、リンク先の先読みとクライアント側での遷移が働き、ページ全体の再読み込みが起きません。

import Link from 'next/link'

export default async function Posts() {
  const posts = await getPosts()

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.slug}>
          <Link href={`/blog/${post.slug}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  )
}

素の a タグを使うとフルリロードになり、レイアウトの状態もスクロール位置も失われます。外部サイトへのリンク以外は Link を使うのが原則です。ボタンのクリック処理の中などから遷移したい場合は useRouter を使いますが、まずは Link で足りないかを検討してください。

6特殊な配置パターン

頻度は下がりますが、覚えておくと設計の幅が広がる規則もあります。

記法意味典型的な用途
@folder名前付きスロット(並行ルート)サイドバーと本文を別々に描画する
(.)folder同じ階層のルートを差し込む一覧の上に詳細をモーダルで重ねる
(..)folder親階層のルートを差し込む親の子ページをオーバーレイ表示する
(...)folderルート直下から差し込む任意のページを現在の画面内に出す

並行ルートを使う場合、バージョン 16 からはすべてのスロットに default.tsx が必須になりました。無いとビルドが失敗します。従来の挙動に合わせるなら、notFound() を呼ぶか null を返すファイルを置きます。

// app/@modal/default.tsx
export default function Default() {
  return null
}

7つまずきやすいところ

  • フォルダを作ったのに 404 になる:page.tsx が無いだけです。フォルダは URL を作るが、公開するのは page か route の役目です。
  • params.slug が undefined になる:await params を忘れています。バージョン 15 まではそのまま読めたため、古い記事のコードが混ざると起きがちです。
  • ルートグループを重ねすぎる:URL に出ないぶん構造が見えにくくなります。レイアウトを分ける目的がないなら普通のフォルダで十分です。
  • 同じ階層に page.tsx と route.ts を置く:競合してビルドが通りません。API は別のパスに切り出します(Route Handlers で API を作る)。

URL の骨格ができたら、次はその周りを取り囲む共通レイアウトと、読み込み中・エラー時の表示を担当する特殊ファイルを見ていきます。

子ページ

子ページはありません

同階層のページ
  1. 開発環境の構築とプロジェクトの初期構成
  2. App Router のルーティングとファイル規約
  3. レイアウトと特殊ファイルの使い分け
  4. サーバーコンポーネントとクライアントコンポーネント
  5. サーバー側でのデータ取得と並列化
  6. ストリーミングで体感速度を上げる
  7. キャッシュの仕組みと use cache
  8. 再検証の使い分け
  9. フォーム送信とデータ更新の実装
  10. エラーハンドリングとエラー境界
  11. Route Handlers で API を作る
  12. メタデータ API で SEO を整える
  13. 画像とフォントの最適化
  14. 本番ビルドとセルフホスト
  15. バージョン16 への移行と注意点

最近更新/作成されたページ