mcpField
N
nextjs-app-router-fundamentals

nextjs-app-router-fundamentals

github.com/wsimmonds開発1092.2k 回インストール53 週間前に更新
リポジトリ

nextjs-app-router-fundamentals について

nextjs-app-router-fundamentalsは、App Routerを使ったNext.js 13+アプリの開発や移行を支援するスキルです。ルーティング規約、レイアウト、メタデータ、データ取得などを解説し、`any`を避けるTypeScriptの指針も示します。

主な機能

  1. 01App Routerのファイルとルーティング規約
  2. 02Pages Routerからの移行ガイド
  3. 03レイアウトとネストされたルート
  4. 04静的・動的メタデータ
  5. 05サーバー・クライアントコンポーネントのパターン
  6. 06データ取得と静的ルート生成

活用シーン

  1. 01Next.js 13+ App Routerプロジェクトの開始
  2. 02Pages Routerからのルート移行
  3. 03ネストされた動的ルートの実装
  4. 04メタデータと静的ページの構築

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/wsimmonds/claude-nextjs-skills --skill nextjs-app-router-fundamentals

インストール後、Claude Code で/nextjs-app-router-fundamentalsを入力して読み込まれたか確認します。

詳細

Next.js App Router の基本

概要

Next.js App Router(Next.js 13 以降)について、Pages Router からの移行、ファイルベースのルーティング規約、レイアウト、メタデータの処理、最新の Next.js パターンを網羅した包括的なガイダンスを提供します。

TypeScript:any 型は絶対に使用しないでください

重要なルール: このコードベースでは @typescript-eslint/no-explicit-any が有効になっています。any を使用するとビルドに失敗します。

❌ 誤り:

function handleSubmit(e: any) { ... }
const data: any[] = [];

✅ 正しい例:

function handleSubmit(e: React.FormEvent<HTMLFormElement>) { ... }
const data: string[] = [];
よく使われる Next.js の型パターン
// Page props
function Page({ params }: { params: { slug: string } }) { ... }
function Page({ searchParams }: { searchParams: { [key: string]: string | string[] | undefined } }) { ... }

// Form events
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { ... }
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { ... }

// Server actions
async function myAction(formData: FormData) { ... }

このスキルを使用する場面

次のような場合にこのスキルを使用します:

  • Pages Router(pages/ ディレクトリ)から App Router(app/ ディレクトリ)に移行する
  • Next.js 13 以降のアプリケーションをゼロから作成する
  • レイアウト、テンプレート、ネストされたルーティングを扱う
  • メタデータと SEO の最適化を実装する
  • App Router のルーティング規約に基づいて構築する
  • ルートグループ、並列ルート、インターセプトルートの基本を扱う

基本概念

App Router と Pages Router

Pages Router(旧式 - Next.js 12 以前):

pages/
├── index.tsx              # Route: /
├── about.tsx              # Route: /about
├── _app.tsx               # Custom App component
├── _document.tsx          # Custom Document component
└── api/                   # API routes
    └── hello.ts           # API endpoint: /api/hello

App Router(最新 - Next.js 13 以降):

app/
├── layout.tsx             # Root layout (required)
├── page.tsx               # Route: /
├── about/                 # Route: /about
│   └── page.tsx
├── blog/
│   ├── layout.tsx         # Nested layout
│   └── [slug]/
│       └── page.tsx       # Dynamic route: /blog/:slug
└── api/                   # Route handlers
    └── hello/
        └── route.ts       # API endpoint: /api/hello
ファイル規約

App Router の特殊ファイル:

  • layout.tsx - セグメントとその子要素で共有される UI(状態を保持し、再レンダリングされません)
  • page.tsx - ルート固有の UI。ルートを一般公開可能にします
  • loading.tsx - React Suspense を使用した読み込み中の UI
  • error.tsx - Error Boundary を使用したエラー UI
  • not-found.tsx - 404 UI
  • template.tsx - layout に似ていますが、ナビゲーション時に再レンダリングされます
  • route.ts - API エンドポイント(Route Handlers)

コロケーション:

  • コンポーネント、テスト、その他のファイルを app/ 内に配置できます
  • 公開ルートを作成するのは page.tsx と route.ts のみです
  • その他のファイル(コンポーネント、ユーティリティ、テスト)はルーティングされません

移行ガイド:Pages Router から App Router へ

ステップ 1:現在の構成を把握する

既存の Pages Router の設定を確認します:

  • pages/ ディレクトリの構成を確認する
  • _app.tsx を特定する - グローバル状態、レイアウト、プロバイダーを処理します
  • _document.tsx を特定する - HTML 構造をカスタマイズします
  • メタデータの使用状況を確認する(next/head、<Head> コンポーネント)
  • すべてのルートと動的セグメントを一覧にする
ステップ 2:ルートレイアウトを作成する

app/layout.tsx を作成します。App Router のすべてのアプリケーションで必須です:

// app/layout.tsx
export const metadata = {
  title: 'My App',
  description: 'App description',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

移行時の注意:

  • _document.tsx の HTML 構造を layout.tsx に移動します
  • _app.tsx のグローバルプロバイダーやラッパーを layout.tsx に移動します
  • <Head> のメタデータを metadata エクスポートに変換します
  • ルートレイアウトには <html> タグと <body> タグを必ず含めてください
ステップ 3:ページをルートに移行する

シンプルなページの移行:

// Before: pages/index.tsx
import Head from 'next/head';

export default function Home() {
  return (
    <>
      <Head>
        <title>Home Page</title>
      </Head>
      <main>
        <h1>Welcome</h1>
      </main>
    </>
  );
}
// After: app/page.tsx
export default function Home() {
  return (
    <main>
      <h1>Welcome</h1>
    </main>
  );
}

// Metadata moved to layout.tsx or exported here
export const metadata = {
  title: 'Home Page',
};

ネストされたルートの移行:

// Before: pages/blog/[slug].tsx
export default function BlogPost() { ... }
// After: app/blog/[slug]/page.tsx
export default function BlogPost() { ... }
ステップ 4:ナビゲーションを更新する

アンカータグを Next.js の Link に置き換えます:

// Before (incorrect in App Router)
<a href="/about">About</a>

// After (correct)
import Link from 'next/link';
<Link href="/about">About</Link>
ステップ 5:Pages ディレクトリを整理する

移行後:

  • pages/ ディレクトリからすべてのページファイルを削除します
  • API ルートをまだ移行しない場合は、pages/api/ を残します
  • _app.tsx と _document.tsx を削除します(機能はレイアウトに移動済みです)
  • 空になった pages/ ディレクトリは必要に応じて削除します

メタデータの処理

静的メタデータ
// app/page.tsx or app/layout.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'My Page',
  description: 'Page description',
  keywords: ['nextjs', 'react'],
  openGraph: {
    title: 'My Page',
    description: 'Page description',
    images: ['/og-image.jpg'],
  },
};
動的メタデータ
// app/blog/[slug]/page.tsx
export async function generateMetadata({
  params
}: {
  params: { slug: string }
}): Promise<Metadata> {
  const post = await getPost(params.slug);

  return {
    title: post.title,
    description: post.excerpt,
  };
}

レイアウトとネスト

ネストされたレイアウトの作成
// app/layout.tsx - Root layout
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Header />
        {children}
        <Footer />
      </body>
    </html>
  );
}

// app/blog/layout.tsx - Blog layout
export default function BlogLayout({ children }) {
  return (
    <div>
      <BlogSidebar />
      <main>{children}</main>
    </div>
  );
}

レイアウトの動作:

  • レイアウトはナビゲーションをまたいで状態を保持します
  • ルート変更時にレイアウトは再レンダリングされません
  • 親レイアウトは子レイアウトをラップします
  • ルートレイアウトは必須で、アプリ全体をラップします

ルーティングパターン

動的ルート
// app/blog/[slug]/page.tsx
export default function BlogPost({
  params
}: {
  params: { slug: string }
}) {
  return <article>Post: {params.slug}</article>;
}
キャッチオールルート
// app/shop/[...slug]/page.tsx - Matches /shop/a, /shop/a/b, etc.
export default function Shop({
  params
}: {
  params: { slug: string[] }
}) {
  return <div>Path: {params.slug.join('/')}</div>;
}
オプショナルキャッチオール
// app/shop/[[...slug]]/page.tsx - Matches /shop AND /shop/a, /shop/a/b
ルートグループ

URL に影響を与えずにルートをグループ化します:

app/
├── (marketing)/
│   ├── about/
│   │   └── page.tsx      # /about
│   └── contact/
│       └── page.tsx      # /contact
└── (shop)/
    └── products/
        └── page.tsx      # /products

よくある移行時の落とし穴

落とし穴 1:ルートレイアウトの HTML タグを忘れる

誤り:

export default function RootLayout({ children }) {
  return <div>{children}</div>; // Missing <html> and <body>
}

正しい例:

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}
落とし穴 2:App Router で next/head を使用する

誤り:

import Head from 'next/head';

export default function Page() {
  return (
    <>
      <Head><title>Title</title></Head>
      <main>Content</main>
    </>
  );
}

正しい例:

export const metadata = { title: 'Title' };

export default function Page() {
  return <main>Content</main>;
}
落とし穴 3:Pages ディレクトリを削除しない

ルートの移行後は、混乱を避けるため古い pages/ ディレクトリのファイルを削除してください。ルートが競合している場合、ビルドに失敗します。

落とし穴 4:page.tsx ファイルの不足

page.tsx ファイルがないと、ルートにはアクセスできません。レイアウトだけではルートは作成されません。

app/
├── blog/
│   ├── layout.tsx   # NOT a route
│   └── page.tsx     # This makes /blog accessible
落とし穴 5:リンクの使い方が正しくない

誤り:

<a href="/about">About</a>  // Works but causes full page reload

正しい例:

import Link from 'next/link';
<Link href="/about">About</Link>  // Client-side navigation

サーバーコンポーネントとクライアントコンポーネント

デフォルト:サーバーコンポーネント

app/ 内のすべてのコンポーネントは、デフォルトでサーバーコンポーネントです。

// app/page.tsx - Server Component (default)
export default async function Page() {
  const data = await fetch('https://api.example.com/data');
  const json = await data.json();

  return <div>{json.title}</div>;
}

メリット:

  • async/await を直接使用できます
  • データベース/API に直接アクセスできます
  • クライアント側の JavaScript が不要です
  • コード分割が自動的に行われます
クライアントコンポーネント

次の機能が必要な場合は、'use client' ディレクティブを使用します。

  • インタラクティブな要素(onClick、onChange など)
  • React フック(useState、useEffect、useContext など)
  • ブラウザー API(window、localStorage など)
  • イベントリスナー
// app/components/Counter.tsx
'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

データ取得パターン

サーバーコンポーネントでのデータ取得
// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 } // Revalidate every hour
  });
  return res.json();
}

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

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}
並列データ取得
export default async function Page() {
  // Fetch in parallel
  const [posts, users] = await Promise.all([
    fetch('https://api.example.com/posts').then(r => r.json()),
    fetch('https://api.example.com/users').then(r => r.json()),
  ]);

  return (/* render */);
}

generateStaticParams による静的サイト生成

概要

generateStaticParams は、Pages Router の getStaticPaths に相当する App Router の機能です。動的ルートの静的ページをビルド時に生成します。

基本的な使い方
// app/blog/[id]/page.tsx
export async function generateStaticParams() {
  // Return array of params to pre-render
  return [
    { id: '1' },
    { id: '2' },
    { id: '3' },
  ];
}

export default function BlogPost({
  params
}: {
  params: { id: string }
}) {
  return <article>Blog post {params.id}</article>;
}

要点:

  • ルートパラメーターのキーを含むオブジェクトの配列を返します
  • 各オブジェクトは、ビルド時に事前レンダリングするページを表します
  • 関数はエクスポートし、generateStaticParams という名前にする必要があります
  • サーバーコンポーネントでのみ動作します('use client' ディレクティブは使用できません)
  • Pages Router の getStaticPaths に代わるものです
静的パラメーター用のデータ取得
// app/blog/[slug]/page.tsx
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,
  }));
}

export default async function BlogPost({
  params
}: {
  params: { slug: string }
}) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json());

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}
複数の動的セグメント
// app/products/[category]/[id]/page.tsx
export async function generateStaticParams() {
  const categories = await getCategories();

  const params = [];
  for (const category of categories) {
    const products = await getProducts(category.slug);
    for (const product of products) {
      params.push({
        category: category.slug,
        id: product.id,
      });
    }
  }

  return params;
}

export default function ProductPage({
  params
}: {
  params: { category: string; id: string }
}) {
  return <div>Category: {params.category}, Product: {params.id}</div>;
}
動的動作の設定
// app/blog/[id]/page.tsx
export async function generateStaticParams() {
  return [{ id: '1' }, { id: '2' }];
}

// Control behavior for non-pre-rendered paths
export const dynamicParams = true; // default - allows runtime generation
// export const dynamicParams = false; // returns 404 for non-pre-rendered paths

export default function BlogPost({
  params
}: {
  params: { id: string }
}) {
  return <article>Post {params.id}</article>;
}

オプション:

  • dynamicParams = true(デフォルト):事前レンダリングされていないパスをオンデマンドで生成します
  • dynamicParams = false:事前レンダリングされていないパスは 404 を返します
一般的なパターン

パターン 1:ID ベースのシンプルなルート

export async function generateStaticParams() {
  return [
    { id: '1' },
    { id: '2' },
    { id: '3' },
  ];
}

パターン 2:API から取得

export async function generateStaticParams() {
  const items = await fetch('https://api.example.com/items').then(r => r.json());
  return items.map(item => ({ id: item.id }));
}

パターン 3:データベースクエリ

export async function generateStaticParams() {
  const posts = await db.post.findMany();
  return posts.map(post => ({ slug: post.slug }));
}
Pages Router からの移行

変更前(Pages Router):

// pages/blog/[id].tsx
export async function getStaticPaths() {
  return {
    paths: [
      { params: { id: '1' } },
      { params: { id: '2' } },
    ],
    fallback: false,
  };
}

export async function getStaticProps({ params }) {
  return { props: { id: params.id } };
}

変更後(App Router):

// app/blog/[id]/page.tsx
export async function generateStaticParams() {
  return [
    { id: '1' },
    { id: '2' },
  ];
}

export const dynamicParams = false; // equivalent to fallback: false

export default function BlogPost({ params }: { params: { id: string } }) {
  return <div>Post {params.id}</div>;
}
避けるべきよくある間違い

❌ 誤り:'use client' を使用する

'use client'; // ERROR! generateStaticParams only works in Server Components

export async function generateStaticParams() {
  return [{ id: '1' }];
}

❌ 誤り:Pages Router のパターンを使用する

export async function getStaticPaths() { // Wrong API!
  return { paths: [...], fallback: false };
}

❌ 誤り:export キーワードがない

async function generateStaticParams() { // Must be exported!
  return [{ id: '1' }];
}

✅ 正しい例:シンプルなサーバーコンポーネント

// app/blog/[id]/page.tsx
// No 'use client' directive

export async function generateStaticParams() {
  return [{ id: '1' }, { id: '2' }];
}

export default function Page({ params }: { params: { id: string } }) {
  return <div>Post {params.id}</div>;
}

実装に関する重要な注意:

generateStaticParams の「作成」または「実装」を依頼された場合:

  • 必ず Edit または Write ツールを使って実際のファイルを変更してください
  • 必ず 既存の page.tsx ファイルに関数を追加してください
  • 必ず generateStaticParams に関する TODO コメントを削除してください
  • コードを Markdown で出力するだけではいけません。実際に実装してください
  • ファイルに書き込まずにコードを提示してはいけません

テストと検証

App Router への移行時または App Router でビルドする際は、次の点を確認してください。

  1. 構成:

    • app/ ディレクトリが存在する
    • <html> と <body> を含むルート layout.tsx が存在する
    • 各ルートに page.tsx ファイルがある
  2. メタデータ:

    • App Router 内に next/head のインポートがない
    • ページまたはレイアウトからメタデータがエクスポートされている
    • メタデータが Metadata 型で正しく型付けされている
  3. ナビゲーション:

    • next/link の Link コンポーネントを使用している
    • 内部ナビゲーションに通常の <a> タグを使用していない
  4. クリーンアップ:

    • pages/ ディレクトリにページファイルが残っていない
    • _app.tsx と _document.tsx が削除されている
    • 古いメタデータのパターンが削除されている

クイックリファレンス

ファイル構成の対応表
Pages RouterApp Router目的
pages/index.tsxapp/page.tsxホームルート
pages/about.tsxapp/about/page.tsxAbout ルート
pages/[id].tsxapp/[id]/page.tsx動的ルート
pages/_app.tsxapp/layout.tsxグローバルレイアウト
pages/_document.tsxapp/layout.tsxHTML 構造
pages/api/hello.tsapp/api/hello/route.tsAPI ルート
よく使うコマンド
# Create new Next.js app with App Router
npx create-next-app@latest my-app

# Run development server
npm run dev

# Build for production
npm run build

# Start production server
npm start

追加リソース

より高度なルーティングパターン(並列ルート、インターセプトルート、ルートハンドラー)については、nextjs-advanced-routing スキルを参照してください。

サーバーコンポーネントとクライアントコンポーネントのベストプラクティスやアンチパターンについては、nextjs-server-client-components および nextjs-anti-patterns スキルを参照してください。

コメント