本記事は筆者の TypeScript + Next.js 学習リポジトリ のコードをベースに、Next.js App Router のルーティングについて体系的に解説した内容となっております。
はじめに
Next.js の App Router(/app ディレクトリ)では、ファイルシステムの構造がそのままURLになるという直感的な設計が採用されています。
本記事では以下の4種類のルーティングを、実際のコードと合わせて解説します。
- 基本ルーティング(ファイルシステムベース)
- 動的ルーティング(Dynamic Routes)
- ルートグループ(Route Groups)
- パラレルルート(Parallel Routes)
- インターセプティングルート(Intercepting Routes)
プロジェクト構成
typescript_nextjs_tutorial/
├─ app/
│ ├─ layout.tsx # 全ページ共通のレイアウト
│ ├─ page.tsx # ルートURL(/)のページ
│ ├─ about/
│ │ └─ page.tsx # /about
│ ├─ blogs/
│ │ └─ [id]/
│ │ └─ page.tsx # /blogs/:id(動的ルーティング)
│ ├─ (admin)/
│ │ └─ dashboard/
│ │ └─ page.tsx # /dashboard(ルートグループ)
│ ├─ parallel/
│ │ ├─ layout.tsx # パラレルルートのレイアウト
│ │ ├─ @team/
│ │ │ └─ page.tsx # スロット: team
│ │ └─ @analytics/
│ │ └─ page.tsx # スロット: analytics
│ └─ feed/
│ └─ page.tsx # インターセプティングルートの起点
└─ ...
1. 基本ルーティング(ファイルシステムベース)
概念
Next.js では /app 配下にフォルダを作り、その中に page.tsx を置くだけでページが完成します。
設定ファイルは一切編集不要で、フォルダ名がそのままURLのパスになります。
ルール
| ファイルパス | URL |
|---|---|
app/page.tsx |
http://localhost:3000/ |
app/about/page.tsx |
http://localhost:3000/about |
app/about/contact/page.tsx |
http://localhost:3000/about/contact |
ディレクトリ構造
app/
├─ page.tsx → /
├─ about/
│ ├─ page.tsx → /about
│ └─ contact/
│ └─ page.tsx → /about/contact (ネストも可能)
コード例
// app/about/page.tsx
export default function AboutPage() {
return <h1>About Page</h1>;
}
ポイント
-
page.tsxという名前のファイルだけがURLとして公開される -
layout.tsx、loading.tsx、error.tsxなどは特別な役割を持つ予約ファイルで、URLにはならない - フォルダをネストすれば
/about/contactのような階層URLも自動生成される
2. 動的ルーティング(Dynamic Routes)
概念
ブログ記事の詳細ページのように、URLの一部が可変になるページを作成できます。
フォルダ名を [paramName] のように角括弧で囲むことで実現します。
ディレクトリ構造
app/
└─ blogs/
└─ [id]/
└─ page.tsx → /blogs/1, /blogs/2, /blogs/abc ...
コード(app/blogs/[id]/page.tsx)
/*
動的ルーティング
URLが可変するページを作成できる
例:http://app/blog/3 http://app/blog/6 など
動的ルーティングの page.tsx では、パラメーターに以下のようなオブジェクトを渡すことができる
- キー :[] で囲んだフォルダ名
- 値 :URLの [] の位置で指定した値
つまりファイルパスが app/blogs/[id] であり、URLが http://app/blogs/3 ならば
{ id: "3" } というオブジェクトが渡される
*/
export default async function BlogPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const {id} = await params;
return <h1>Blog ID: {id}</h1>;
}
URLとparamsの対応
| URL |
params の値 |
|---|---|
/blogs/1 |
{ id: "1" } |
/blogs/42 |
{ id: "42" } |
/blogs/typescript |
{ id: "typescript" } |
複数のパラメーター
フォルダをさらにネストすれば複数のパラメーターも扱えます。
app/
└─ blogs/
└─ [category]/
└─ [id]/
└─ page.tsx → /blogs/tech/42
// params: { category: "tech", id: "42" }
export default async function BlogPage({
params,
}: {
params: Promise<{ category: string; id: string }>;
}) {
const { category, id } = await params;
return <h1>{category} - {id}</h1>;
}
キャッチオールルート
[...slug] を使うと、可変長のパスセグメントをまとめて受け取れます。
app/docs/[...slug]/page.tsx
| URL |
params の値 |
|---|---|
/docs/a |
{ slug: ["a"] } |
/docs/a/b/c |
{ slug: ["a", "b", "c"] } |
3. ルートグループ(Route Groups)
概念
フォルダ名を (groupName) のように括弧で囲むと、URLのパスに影響を与えずにファイルをグループ化できます。
レイアウトの共有範囲を制御したり、管理画面と一般ページを論理的に分けたりするのに役立ちます。
ディレクトリ構造
app/
├─ (admin)/
│ ├─ layout.tsx # 管理画面専用レイアウト
│ ├─ dashboard/
│ │ └─ page.tsx → /dashboard ("(admin)" はURLに含まれない)
│ └─ settings/
│ └─ page.tsx → /settings
└─ (public)/
├─ layout.tsx # 一般ページ専用レイアウト
└─ about/
└─ page.tsx → /about
コード(app/(admin)/dashboard/page.tsx)
// ルートグループ (admin) はURLに含まれない
// このページのURLは /dashboard になる
export default function DashboardPage() {
return <h1>Dashboard</h1>;
}
ルートグループ専用レイアウト
// app/(admin)/layout.tsx
// 管理画面グループ内のページにのみ適用されるレイアウト
export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<nav>管理画面ナビゲーション</nav>
<main>{children}</main>
</div>
);
}
ポイント
-
(admin)(public)などのフォルダ名はURLに一切反映されない - グループごとに異なる
layout.tsxを持てるため、管理画面と一般ページで異なるヘッダーを適用できる - 単純なファイル整理にも使える
4. パラレルルート(Parallel Routes)
概念
1つのページ内に複数の独立したコンポーネント(スロット)を同時に表示できる機能です。
ダッシュボードのようにチーム情報・分析データなど複数のセクションを非同期で表示する場面で活躍します。
ディレクトリ構造
フォルダ名に @ を付けたフォルダがスロットになります。
app/parallel/
├─ layout.tsx # スロットを受け取るレイアウト
├─ page.tsx # children として表示される
├─ @team/
│ └─ page.tsx # team スロット
└─ @analytics/
└─ page.tsx # analytics スロット
コード(app/parallel/layout.tsx)
/*
パラレルルート
1つのルートの中に複数の独立したコンポーネントを配置することができる
使用方法:フォルダ名に @ を付けて、その中にpage.tsxを置く
- app/parallel/layout.tsx でメインレイアウトを作成
- children とは別に team, analytics というパラメーターを追加
※ children は parallel/page.tsx を描画する
- /parallel/@team, @analytics フォルダ(スロット)を作成し、それぞれに page.tsx を置く
- layout.tsx はパラメーター名とスロット名を照合し、一致するコンポーネントを非同期で配置する
*/
export default function ParallelLayout({
children,
team, // @team/page.tsx を受け取る
analytics, // @analytics/page.tsx を受け取る
}: {
children: React.ReactNode;
team: React.ReactNode;
analytics: React.ReactNode;
}) {
return (
<div>
<h1>Parallel Routes Page</h1>
<div>{children}</div>
<div style={{ display: 'flex', gap: '20px' }}>
<div style={{ border: '1px solid red' }}>{team}</div>
<div style={{ border: '1px solid blue' }}>{analytics}</div>
</div>
</div>
);
}
スロットのページコンポーネント
// app/parallel/@team/page.tsx
export default function TeamSlot() {
return <p>チームメンバー一覧</p>;
}
// app/parallel/@analytics/page.tsx
export default function AnalyticsSlot() {
return <p>アクセス解析データ</p>;
}
画面イメージ
┌──────────────────────────────────┐
│ Parallel Routes Page │ ← layout.tsx
│ │
│ children(page.tsx の内容) │
│ │
│ ┌───────────┐ ┌───────────────┐ │
│ │ @team │ │ @analytics │ │ ← スロット(独立して非同期で描画)
│ └───────────┘ └───────────────┘ │
└──────────────────────────────────┘
ポイント
-
@フォルダ名はURLに影響しない(/parallelのみ) - 各スロットは独立して描画されるため、一方の読み込みが遅くても他方には影響しない
-
layout.tsxだけでなくpage.tsxにスロットを配置することも可能
5. インターセプティングルート(Intercepting Routes)
概念
リンクをクリックしたときとURLに直接アクセスしたときで、表示するコンポーネントを切り替える機能です。
Instagramのような「フィード上の写真をクリックするとモーダルで開くが、URLを直接開くと専用ページが表示される」という体験を実現できます。
ディレクトリ構造
app/
├─ feed/
│ └─ page.tsx # リンクを設置したページ
├─ photo/
│ └─ [id]/
│ └─ page.tsx # 直接アクセス時の通常ページ
└─ feed/
└─ (..)photo/ # インターセプト用フォルダ
└─ [id]/
└─ page.tsx # リンククリック時に表示(モーダルなど)
コード(app/feed/page.tsx)
/*
インターセプティングルート
ページ内のリンク等をクリックした際、URLは変わるがページ遷移はせずにコンテンツが切り替わり
ページリロードをすると、URLの本来のページが描画される挙動を作れる機能
使用方法:
1. リンクを実装したページ(feed)と遷移先のページ(photo/[id])を作成
2. リンクを実装したページフォルダ直下に (..)遷移先フォルダ名 を作成
3. そのフォルダ直下に page.tsx を作成する
これにより:
リンククリック → (..)photo/[id]/page.tsx → リロード → photo/[id]/page.tsx
という挙動になる
※ URLに直接アクセスした場合は、(..)~/page.tsx は介さない
※ (..) は1つ上の階層の場合で、他にも以下のように指定できる
. = 同じ階層
(..)(..) = 2つ上の階層
(...) = app ルートから
*/
import Link from "next/link";
export default function FeedPage() {
return (
<div>
<h1>Feed Page</h1>
<Link href='/photo/1'>Go to Photo 1</Link>
</div>
);
}
インターセプト用ページ(モーダルとして表示)
// app/feed/(..)photo/[id]/page.tsx
export default function PhotoModal({ params }: { params: { id: string } }) {
return (
<div style={{
position: 'fixed', inset: 0,
background: 'rgba(0,0,0,0.5)',
display: 'flex', alignItems: 'center', justifyContent: 'center'
}}>
<div style={{ background: 'white', padding: '2rem' }}>
<p>モーダルで表示中: Photo {params.id}</p>
</div>
</div>
);
}
通常ページ(直接アクセス時)
// app/photo/[id]/page.tsx
export default function PhotoPage({ params }: { params: { id: string } }) {
return <h1>Photo {params.id} の詳細ページ</h1>;
}
動作フロー
フィードページ(/feed)でリンクをクリック
↓
URL: /photo/1 に変わる
↓
(..)photo/[id]/page.tsx が表示(モーダルなど)
↓
リロード or URLを直接入力
↓
photo/[id]/page.tsx が表示(通常のページ)
階層指定の記法
| 記法 | 意味 |
|---|---|
(.)folderName |
同じ階層 |
(..)folderName |
1つ上の階層 |
(..)(..)folderName |
2つ上の階層 |
(...)folderName |
app ルートから |
まとめ
| ルーティングの種類 | 記法 | ユースケース |
|---|---|---|
| 基本ルーティング | folder/page.tsx |
通常のページ作成 |
| 動的ルーティング | [param]/page.tsx |
詳細ページ・記事ページ |
| ルートグループ | (group)/folder/ |
レイアウト分離・ファイル整理 |
| パラレルルート | @slot/page.tsx |
ダッシュボード・複数セクション |
| インターセプティングルート | (..)folder/page.tsx |
モーダル・コンテキスト切替 |
Next.js App Router のルーティングは、ファイルとフォルダの命名規則だけで多彩な画面遷移を実現できるのが大きな魅力です。
まずは基本ルーティングと動的ルーティングをマスターし、必要に応じてパラレルルートやインターセプティングルートへと応用していきましょう。