mcpField
N
nextjs-app-router-expert

nextjs-app-router-expert

github.com/curiositech開発257165 回インストール2 日前に更新
リポジトリ

nextjs-app-router-expert について

Next.js 14/15のApp Routerを使ったアプリの開発とデバッグを支援するスキルです。ルーティング、React Server Components、データ取得、キャッシュ、Server Actions、ミドルウェア、パフォーマンスを扱います。

主な機能

  1. 01ファイルベースと高度なルーティング
  2. 02React Server Componentsとストリーミング
  3. 03データ取得とキャッシュ戦略
  4. 04Server Actionsによるフォーム更新
  5. 05認証やリダイレクト用ミドルウェア
  6. 06レンダリングとバンドルの最適化

活用シーン

  1. 01App Routerプロジェクトの開始
  2. 02Pages Routerからの移行
  3. 03モーダルや並列ルートの構築
  4. 04hydrationやRSCの問題の調査

インストール

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

$npx skills add https://github.com/curiositech/some_claude_skills --skill nextjs-app-router-expert

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

同梱ファイル.claude-plugin/plugin.json

詳細

Next.js App Router エキスパート

概要

Next.js 14/15 の App Router アーキテクチャ、React Server Components(RSC)、Server Actions、最新のフルスタック React 開発に精通しています。ルーティングパターン、データ取得戦略、キャッシュ、ストリーミング、デプロイの最適化を専門としています。

使用する場面

  • App Router を使用して新しい Next.js プロジェクトを開始するとき
  • Pages Router から App Router に移行するとき
  • 複雑なルーティングパターン(並列ルート、インターセプトルート)を実装するとき
  • RSC とキャッシュを使ってデータ取得を最適化するとき
  • ミューテーション用に Server Actions を設定するとき
  • 認証やリダイレクト用にミドルウェアを設定するとき
  • ハイドレーションエラーや RSC の問題をデバッグするとき
  • Vercel、Cloudflare、またはセルフホスト環境にデプロイするとき

機能

ルーティングアーキテクチャ
  • app/ ディレクトリを使ったファイルベースのルーティング
  • 動的ルート([slug]、[...catchAll]、[[...optional]])
  • 整理用のルートグループ (group)
  • 並列ルート @modal、@sidebar
  • インターセプトルート (.)、(..)、(..)(..)
  • ルートセグメントごとのローディング境界とエラー境界
React Server Components
  • Server コンポーネントと Client コンポーネントの境界
  • 'use client' ディレクティブの配置
  • コンポジションパターン(Server が Client をラップする構成)
  • Suspense 境界を使ったストリーミング
  • プログレッシブレンダリング戦略
データ取得
  • 自動重複排除機能を備えた fetch()
  • キャッシュ戦略(force-cache、no-store、revalidate)
  • 静的生成用の generateStaticParams()
  • インクリメンタル静的再生成(ISR)
  • revalidatePath() / revalidateTag() を使ったオンデマンド再検証
Server Actions
  • 'use server' を使ったフォームのミューテーション
  • useOptimistic を使った楽観的更新
  • プログレッシブエンハンスメント(JavaScript がなくても動作)
  • エラー処理とバリデーション
  • ミューテーション後のリダイレクト
Middleware と Edge
  • 認証、リダイレクト、リライト用の middleware.ts
  • Edge Runtime と Node.js Runtime
  • 位置情報と条件付きルーティング
  • A/B テストとフィーチャーフラグ
パフォーマンスの最適化
  • next/image を使った画像の最適化
  • next/font を使ったフォントの最適化
  • スクリプトの読み込み戦略
  • バンドル分析とコード分割
  • 部分的なプリレンダリング(PPR)

依存関係

以下と相性よく動作します:

  • react-performance-optimizer - React 固有のパフォーマンスパターン
  • vercel-deployment - Vercel のデプロイ設定
  • cloudflare-worker-dev - Edge へのデプロイパターン
  • postgresql-optimization - RSC 用のデータベースクエリ

例

基本的なルート構成
app/
├── layout.tsx          # Root layout (required)
├── page.tsx            # Home page (/)
├── loading.tsx         # Loading UI
├── error.tsx           # Error boundary
├── not-found.tsx       # 404 page
├── blog/
│   ├── page.tsx        # /blog
│   └── [slug]/
│       ├── page.tsx    # /blog/:slug
│       └── loading.tsx # Per-route loading
└── (auth)/             # Route group (no URL impact)
    ├── login/
    │   └── page.tsx    # /login
    └── register/
        └── page.tsx    # /register
データを取得する Server コンポーネント
// app/posts/page.tsx
import { Suspense } from 'react';

async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 }, // ISR: revalidate every hour
  });
  return res.json();
}

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

  return (
    <main>
      <h1>Blog Posts</h1>
      <Suspense fallback={<PostsSkeleton />}>
        <PostList posts={posts} />
      </Suspense>
    </main>
  );
}
Server Action フォーム
// app/contact/page.tsx
import { redirect } from 'next/navigation';
import { revalidatePath } from 'next/cache';

async function submitContact(formData: FormData) {
  'use server';

  const email = formData.get('email') as string;
  const message = formData.get('message') as string;

  // Validate
  if (!email || !message) {
    throw new Error('Email and message required');
  }

  // Save to database
  await db.contacts.create({ email, message });

  // Revalidate and redirect
  revalidatePath('/contact');
  redirect('/contact/success');
}

export default function ContactPage() {
  return (
    <form action={submitContact}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      <button type="submit">Send</button>
    </form>
  );
}
並列ルート(モーダルパターン)
app/
├── layout.tsx
├── page.tsx
├── @modal/
│   ├── default.tsx     # Empty state when no modal
│   └── (.)photo/[id]/
│       └── page.tsx    # Intercept /photo/[id] as modal
└── photo/[id]/
    └── page.tsx        # Full page when direct navigation
// app/layout.tsx
export default function Layout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <>
      {children}
      {modal}
    </>
  );
}
認証用ミドルウェア
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token');
  const isAuthPage = request.nextUrl.pathname.startsWith('/login');
  const isProtectedPage = request.nextUrl.pathname.startsWith('/dashboard');

  // Redirect authenticated users away from login
  if (isAuthPage && token) {
    return NextResponse.redirect(new URL('/dashboard', request.url));
  }

  // Redirect unauthenticated users to login
  if (isProtectedPage && !token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/login'],
};
動的パラメーターを使った静的生成
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';

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 async function generateMetadata({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);

  return {
    title: post?.title ?? 'Post Not Found',
    description: post?.excerpt,
  };
}

export default async function PostPage({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);

  if (!post) {
    notFound();
  }

  return (
    <article>
      <h1>{post.title}</h1>
      {/* NOTE: Always sanitize HTML content with DOMPurify before rendering */}
      <div>{post.content}</div>
    </article>
  );
}
Suspense を使ったストリーミング
// app/dashboard/page.tsx
import { Suspense } from 'react';

export default function DashboardPage() {
  return (
    <div className="grid grid-cols-2 gap-4">
      {/* These load in parallel and stream in as ready */}
      <Suspense fallback={<CardSkeleton />}>
        <RevenueCard />
      </Suspense>
      <Suspense fallback={<CardSkeleton />}>
        <UsersCard />
      </Suspense>
      <Suspense fallback={<TableSkeleton />}>
        <RecentOrders />
      </Suspense>
    </div>
  );
}

// Each component fetches its own data
async function RevenueCard() {
  const revenue = await getRevenue(); // Server-side fetch
  return <Card title="Revenue" value={revenue} />;
}

ベストプラクティス

  1. デフォルトでは Server コンポーネントを使う - インタラクティブ性に必要な場合のみ 'use client' を追加します
  2. データ取得を近くに配置する - データが必要なコンポーネント内でデータを取得します
  3. Suspense 境界を使う - ストリーミングのために非同期コンポーネントをラップします
  4. キャッシュを活用する - 効率的にキャッシュするため、revalidate とタグを使います
  5. プログレッシブエンハンスメント - Server Actions は JavaScript がなくても動作します
  6. 整理にはルートグループを使う - URL に影響を与えず整理するため、(folder) を使います
  7. セグメントごとにエラー境界を設ける - 重要なルートに error.tsx を追加します
  8. Metadata API - 動的な SEO には generateMetadata を使います
  9. ユーザーコンテンツをサニタイズする - HTML をレンダリングする際は、必ず DOMPurify などを使います

よくある落とし穴

  • ハイドレーションの不一致 - Server と Client のレンダリングの違い(日付、ランダム値)
  • 'use client' の使いすぎ - コンポーネントツリーの上位に Client 境界を置きすぎる
  • ウォーターフォール型のデータ取得 - 独立したデータ取得を並列化していない
  • ローディング状態の不足 - loading.tsx や Suspense 境界を忘れる
  • 古いデータ - ミューテーション後にキャッシュを無効化していない
  • 大きなクライアントバンドル - Client コンポーネントでサーバー専用コードをインポートする
  • XSS の脆弱性 - サニタイズされていないユーザー入力の HTML をレンダリングする

コメント