NextJS

Next.js 完全ガイド:フルスタックReactフレームワークの全貌


1. はじめに

1.1 Next.js とは

Next.js は、Vercel 社が開発・メンテナンスを行うオープンソースの React フレームワークである。現在の最新バージョンは 16.2.2 であり、React をベースとしたフルスタック Web アプリケーション開発のためのデファクトスタンダードとして広く採用されている。

従来の React はクライアントサイドレンダリング(CSR)のみをサポートする UI ライブラリであったが、Next.js はサーバーサイドレンダリング(SSR)、静的サイト生成(SSG)、インクリメンタル静的再生成(ISR)など、多様なレンダリング戦略を統合的に提供する。これにより、開発者はページやコンポーネント単位で最適なレンダリング方式を選択できる。

1.2 React フレームワークとしての位置づけ

React 自体は「UI を構築するための JavaScript ライブラリ」であり、ルーティング、データフェッチング、ビルド最適化などの機能は含まれていない。Next.js はこれらのギャップを埋め、以下のような機能を包括的に提供する。

機能カテゴリ素の ReactNext.js
ルーティング外部ライブラリ(React Router 等)が必要ファイルシステムベースルーティング(組込み)
サーバーサイドレンダリング手動で構築が必要SSR / SSG / ISR を標準サポート
API エンドポイント別途バックエンドサーバーが必要Route Handlers で統合
画像最適化手動実装next/image コンポーネント
フォント最適化手動実装next/font モジュール
バンドル最適化Webpack / Vite の設定が必要自動コード分割・ツリーシェイキング
TypeScript サポート手動設定ゼロコンフィグ対応
ミドルウェアExpress 等が必要Edge Middleware 内蔵

1.3 Next.js の歴史と進化

Next.js は 2016 年に初版がリリースされて以来、急速に進化を遂げてきた。特に大きな転換点となったのは以下のバージョンである。

  • Next.js 9(2019年):API Routes の導入により、フルスタック開発が可能に
  • Next.js 10(2020年)next/image コンポーネント、自動画像最適化の追加
  • Next.js 12(2021年):Middleware の導入、Rust ベースの SWC コンパイラ採用
  • Next.js 13(2022年):App Router の導入、React Server Components の統合
  • Next.js 14(2023年):Server Actions の安定版リリース、Turbopack の改良
  • Next.js 15(2024年)'use cache' ディレクティブ、非同期リクエスト API
  • Next.js 16(2025年):Turbopack の完全統合、パフォーマンスの大幅改善

1.4 プロジェクトの作成

Next.js プロジェクトは create-next-app CLI を使用して簡単に開始できる。

# 最新版で新規プロジェクトを作成
npx create-next-app@latest my-nextjs-app

# TypeScript テンプレートで作成(デフォルトで有効)
npx create-next-app@latest my-nextjs-app --typescript

# 対話的にオプションを選択
npx create-next-app@latest my-nextjs-app \
  --typescript \
  --tailwind \
  --eslint \
  --app \
  --src-dir \
  --import-alias "@/*"

作成されるプロジェクト構造は以下の通りである。

my-nextjs-app/
├── app/
│   ├── layout.tsx          # ルートレイアウト
│   ├── page.tsx            # ホームページ
│   ├── globals.css         # グローバルスタイル
│   └── favicon.ico         # ファビコン
├── public/                 # 静的アセット
├── next.config.js          # Next.js 設定ファイル
├── package.json            # 依存関係
├── tsconfig.json           # TypeScript 設定
└── tailwind.config.ts      # Tailwind CSS 設定

1.5 開発サーバーの起動

npm run dev              # 開発モード(ホットリロード有効)
npm run dev --turbopack  # Turbopack を使用した高速開発モード
npm run build            # ビルド
npm run start            # プロダクションモードで起動

2. アーキテクチャ概要

2.1 App Router と Pages Router

Next.js には 2 つのルーターシステムが存在する。

App Router(推奨)

App Router は Next.js 13 で導入された新しいルーティングシステムであり、app/ ディレクトリを使用する。React Server Components をネイティブにサポートし、以下の特徴を持つ。

  • React Server Components:デフォルトでサーバーコンポーネントとして動作
  • ネストレイアウトlayout.tsx による階層的なレイアウト管理
  • ストリーミングloading.tsx による段階的な UI レンダリング
  • Server Actions'use server' ディレクティブによるサーバー側のデータ変更
  • 並列ルート:同一レイアウト内での複数ページの同時表示
  • インターセプトルート:モーダルパターンの実現

Pages Router(レガシー)

Pages Router は pages/ ディレクトリを使用する従来のルーティングシステムである。後方互換性のために引き続きサポートされているが、新規プロジェクトでは App Router の使用が推奨される。

両ルーターの比較

特徴App RouterPages Router
ディレクトリapp/pages/
デフォルトコンポーネントServer ComponentsClient Components
レイアウトネストレイアウト(layout.tsx_app.tsx のみ
データフェッチングasync コンポーネント + fetchgetServerSideProps / getStaticProps
ローディング UIloading.tsx(ストリーミング)手動実装
エラーハンドリングerror.tsx(Error Boundary)_error.tsx
API ルートroute.ts(Route Handlers)pages/api/
メタデータmetadata オブジェクト / generateMetadatanext/head
Server Actionsサポート非サポート

2.2 ファイルシステムベースルーティング

ディレクトリ構造が URL パスに直接マッピングされるため、ルーティング設定ファイルが不要である。

app/
├── page.tsx                        # /
├── about/
│   └── page.tsx                    # /about
├── blog/
│   ├── page.tsx                    # /blog
│   ├── [slug]/
│   │   └── page.tsx                # /blog/:slug
│   └── [...categories]/
│       └── page.tsx                # /blog/*(キャッチオール)
├── dashboard/
│   ├── layout.tsx                  # ダッシュボード共通レイアウト
│   ├── page.tsx                    # /dashboard
│   └── settings/
│       └── page.tsx                # /dashboard/settings
└── (marketing)/                    # ルートグループ(URLに影響しない)
    ├── layout.tsx
    └── pricing/
        └── page.tsx                # /pricing

2.3 ファイル規約(File Conventions)

ファイル名役割説明
page.tsxページルートの UI。存在するディレクトリが公開ルートになる
layout.tsxレイアウト子ルートで共有される UI ラッパー。状態が保持される
template.tsxテンプレートlayout.tsx に似るが、ナビゲーション毎に再マウント
loading.tsxローディングSuspense ベースのローディング UI
error.tsxエラーError Boundary としてのエラー UI
not-found.tsx404ルートが見つからない場合の UI
route.tsAPI ルートサーバーサイドの API エンドポイント
default.tsxデフォルトパラレルルートのフォールバック UI
global-error.tsxグローバルエラールートレイアウトのエラーハンドリング
middleware.tsミドルウェアリクエスト前のインターセプト処理

2.4 レンダリングアーキテクチャ

┌─────────────────────────────────────────────────────────────┐
│                       サーバー側                              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  Server Components(デフォルト)                         │  │
│  │  - データフェッチング(async/await)                      │  │
│  │  - バックエンドリソースへの直接アクセス                     │  │
│  │  - 機密情報(API キー、トークン等)の安全な利用             │  │
│  │  - 大きな依存関係をサーバー側に保持(バンドル削減)          │  │
│  └────────────────────────────────────────────────────────┘  │
│                   RSC Payload                                │
├──────────────────────────────────────────────────────────────┤
│                      クライアント側                            │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  Client Components('use client')                      │  │
│  │  - インタラクティブ UI(onClick, onChange等)              │  │
│  │  - ブラウザ API の使用(window, document等)              │  │
│  │  - React Hooks の使用(useState, useEffect等)           │  │
│  └────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

3. ルーティングシステム

3.1 基本的なルーティング

// app/page.tsx - ルートページ(/)
export default function HomePage() {
  return (
    <main>
      <h1>ホームページへようこそ</h1>
      <p>Next.js で構築されたアプリケーションです。</p>
    </main>
  );
}

3.2 レイアウト(layout.tsx)

// app/layout.tsx - ルートレイアウト(必須)
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
  title: 'My Next.js App',
  description: 'Next.js で構築されたアプリケーション',
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ja">
      <body className={inter.className}>
        <header>
          <nav>
            <a href="/">ホーム</a>
            <a href="/about">About</a>
            <a href="/blog">ブログ</a>
          </nav>
        </header>
        <main>{children}</main>
        <footer><p>&copy; 2026 My Next.js App</p></footer>
      </body>
    </html>
  );
}
// app/dashboard/layout.tsx - ネストされたレイアウト
import { Sidebar } from '@/components/Sidebar';

export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex">
      <Sidebar />
      <div className="flex-1 p-6">{children}</div>
    </div>
  );
}

3.3 テンプレート(template.tsx)

テンプレートはレイアウトに似ているが、ナビゲーション毎に新しいインスタンスが作成される。

// app/dashboard/template.tsx
'use client';
import { useEffect } from 'react';

export default function DashboardTemplate({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    trackPageView(window.location.pathname);
  }, []);
  return <div className="animate-fade-in">{children}</div>;
}

3.4 動的ルート

// app/blog/[slug]/page.tsx - 単一の動的セグメント
interface BlogPostProps {
  params: Promise<{ slug: string }>;
}

export default async function BlogPostPage({ params }: BlogPostProps) {
  const { slug } = await params;
  const post = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { revalidate: 3600 },
  }).then((res) => res.json());

  return (
    <article>
      <h1>{post.title}</h1>
      <time dateTime={post.publishedAt}>
        {new Date(post.publishedAt).toLocaleDateString('ja-JP')}
      </time>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

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 }));
}
// app/shop/[...categories]/page.tsx - キャッチオールセグメント
export default async function ShopPage({ params }: { params: Promise<{ categories: string[] }> }) {
  const { categories } = await params;
  // /shop/clothes/tops => categories = ['clothes', 'tops']
  return <div><h1>ショップ: {categories.join(' > ')}</h1></div>;
}
// app/docs/[[...slug]]/page.tsx - オプショナルキャッチオール
// /docs と /docs/getting-started の両方にマッチ
export default async function DocsPage({ params }: { params: Promise<{ slug?: string[] }> }) {
  const { slug } = await params;
  if (!slug) return <h1>ドキュメント トップページ</h1>;
  return <div><h1>{slug.join('/')}</h1></div>;
}

3.5 ルートグループ

括弧 () で囲んだフォルダ名で作成され、URL 構造に影響を与えずにルートを整理できる。

app/
├── (marketing)/         # /pricing, /about に影響なし
│   ├── layout.tsx       # マーケティング用レイアウト
│   └── pricing/page.tsx # /pricing
├── (app)/               # /dashboard, /settings に影響なし
│   ├── layout.tsx       # アプリ用レイアウト
│   └── dashboard/page.tsx # /dashboard
└── (auth)/              # /login, /register に影響なし
    ├── layout.tsx       # 認証用レイアウト
    └── login/page.tsx   # /login

3.6 パラレルルート

同一レイアウト内で複数のページを同時にレンダリングする。@ プレフィックスのフォルダで定義。

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children, analytics, team, notifications,
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
  notifications: React.ReactNode;
}) {
  return (
    <div className="grid grid-cols-12 gap-4 p-6">
      <div className="col-span-8">{children}</div>
      <div className="col-span-4 space-y-4">
        <div>{analytics}</div>
        <div>{team}</div>
        <div>{notifications}</div>
      </div>
    </div>
  );
}

3.7 インターセプトルート

現在のレイアウト内で別のルートのコンテンツを表示する。モーダルパターンに有用。

規約意味
(.)同じレベルのセグメントをインターセプト
(..)1つ上のレベルをインターセプト
(..)(..)2つ上のレベルをインターセプト
(...)ルートからインターセプト

3.8 ローディングとエラーハンドリング

// app/blog/loading.tsx
export default function BlogLoading() {
  return (
    <div className="space-y-4">
      {[...Array(5)].map((_, i) => (
        <div key={i} className="animate-pulse">
          <div className="h-6 bg-gray-200 rounded w-3/4 mb-2" />
          <div className="h-4 bg-gray-200 rounded w-1/2" />
        </div>
      ))}
    </div>
  );
}
// app/blog/error.tsx
'use client';
import { useEffect } from 'react';

export default function BlogError({ error, reset }: { error: Error; reset: () => void }) {
  useEffect(() => { console.error('Blog error:', error); }, [error]);
  return (
    <div>
      <h2>エラーが発生しました</h2>
      <p>{error.message}</p>
      <button onClick={reset}>再試行</button>
    </div>
  );
}
// app/blog/[slug]/not-found.tsx
import Link from 'next/link';
export default function BlogNotFound() {
  return (
    <div>
      <h2>記事が見つかりません</h2>
      <Link href="/blog">ブログ一覧に戻る</Link>
    </div>
  );
}

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

4.1 サーバーコンポーネント(Server Components)

App Router ではデフォルトで サーバーコンポーネント として動作する。

// app/posts/page.tsx - サーバーコンポーネント(デフォルト)
import { db } from '@/lib/database';

export default async function PostsPage() {
  const posts = await db.post.findMany({
    orderBy: { createdAt: 'desc' },
    include: { author: true },
  });

  return (
    <div>
      <h1>最新の投稿</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>著者: {post.author.name}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

利点:データフェッチング、セキュリティ、キャッシュ、バンドルサイズ削減、SEO、ストリーミング。

4.2 クライアントコンポーネント(Client Components)

// app/components/Counter.tsx
'use client';
import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count - 1)}>-</button>
      <span>{count}</span>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

4.3 使い分け

機能サーバークライアント
データフェッチング(async/await)可能不可
機密情報の利用安全不可
useState / useEffect不可可能
イベントリスナー不可可能
ブラウザ API不可可能

4.4 コンポーネント構成パターン

// パターン1: サーバーからクライアントにデータを渡す
// app/posts/page.tsx (Server)
import PostList from './PostList';
export default async function PostsPage() {
  const posts = await db.post.findMany();
  return <PostList initialPosts={posts} />;
}

// パターン2: children パターン
// app/components/InteractiveWrapper.tsx (Client)
'use client';
import { useState } from 'react';
export default function InteractiveWrapper({ children }: { children: React.ReactNode }) {
  const [isVisible, setIsVisible] = useState(true);
  return (
    <div>
      <button onClick={() => setIsVisible(!isVisible)}>{isVisible ? '隠す' : '表示'}</button>
      {isVisible && children}
    </div>
  );
}

// パターン3: サードパーティラッパー
// app/components/ThemeProvider.tsx
'use client';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

5. データフェッチング

5.1 SSR / SSG / ISR / CSR

// SSR - リクエスト毎にデータ取得
const res = await fetch('https://api.example.com/news', { cache: 'no-store' });

// SSG - ビルド時にデータ取得(デフォルト)
const res = await fetch('https://api.example.com/products', { cache: 'force-cache' });

// ISR - 定期的に再検証
const res = await fetch('https://api.example.com/posts', { next: { revalidate: 60 } });

// タグベースキャッシュ
const res = await fetch('https://api.example.com/posts', { next: { tags: ['posts'] } });

5.2 タグベースの再検証

'use server';
import { revalidateTag, revalidatePath } from 'next/cache';

export async function revalidatePosts() { revalidateTag('posts'); }
export async function revalidatePost(id: string) { revalidateTag(`post-${id}`); }
export async function revalidateBlogPath() { revalidatePath('/blog'); }

5.3 並列フェッチ

export default async function DashboardPage() {
  const [user, orders, notifications] = await Promise.all([
    getUser('user-123'),
    getOrders('user-123'),
    getNotifications('user-123'),
  ]);
  return <div><h1>こんにちは、{user.name} さん</h1></div>;
}

5.4 ストリーミングとSuspense

import { Suspense } from 'react';

export default function DashboardPage() {
  return (
    <div>
      <h1>ダッシュボード</h1>
      <Suspense fallback={<div className="animate-pulse h-32 bg-gray-200" />}>
        <DashboardStats />
      </Suspense>
      <Suspense fallback={<OrdersSkeleton />}>
        <RecentOrders />
      </Suspense>
    </div>
  );
}

5.5 ORM / データベース統合

// lib/database.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined };
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

6. キャッシュシステム

6.1 4層キャッシュアーキテクチャ

┌──────────────────────────────────────────────────────────────┐
│  クライアント: 4. Router Cache(RSC Payload のブラウザ内キャッシュ)│
├──────────────────────────────────────────────────────────────┤
│  サーバー:                                                    │
│    3. Full Route Cache(HTML/RSC Payload のサーバーキャッシュ)  │
│    2. Data Cache(fetch レスポンスの永続キャッシュ)             │
│    1. Request Memoization(同一リクエスト内の重複排除)          │
└──────────────────────────────────────────────────────────────┘

6.2 fetchオプション

await fetch(url);                                    // デフォルト(キャッシュ)
await fetch(url, { cache: 'force-cache' });          // 明示的キャッシュ
await fetch(url, { cache: 'no-store' });             // キャッシュしない
await fetch(url, { next: { revalidate: 3600 } });    // 時間ベース再検証
await fetch(url, { next: { tags: ['my-data'] } });   // タグベースキャッシュ

6.3 再検証

'use server';
import { revalidatePath, revalidateTag } from 'next/cache';

// パスベース
revalidatePath('/blog');
revalidatePath('/blog', 'layout');

// タグベース
revalidateTag('posts');
revalidateTag(`post-${id}`);

6.4 Draft Mode

import { draftMode } from 'next/headers';

// 有効化
const draft = await draftMode();
draft.enable();

// 判定
const { isEnabled } = await draftMode();
const post = isEnabled ? await fetchDraftPost(slug) : await fetchPublishedPost(slug);

6.5 'use cache' ディレクティブ

'use cache';
export default async function CachedComponent() {
  const data = await fetchExpensiveData();
  return <div>{data.result}</div>;
}

7. レンダリング戦略

戦略レンダリング時点データの鮮度パフォーマンス用途
SSGビルド時ビルド時点最高速ドキュメント、ブログ
ISRビルド時 + バックグラウンド再生成定期更新高速EC、ニュース
SSRリクエスト時常に最新中程度ダッシュボード
CSRクライアント実行取得時初期表示遅SPA、管理画面

7.1 ルートセグメント設定

export const dynamic = 'auto' | 'force-dynamic' | 'force-static' | 'error';
export const revalidate = false | 0 | 60;
export const runtime = 'nodejs' | 'edge';
export const maxDuration = 30;

7.2 Partial Prerendering(PPR)

静的部分と動的部分を同一ルートで共存させる実験的機能。

const nextConfig = { experimental: { ppr: true } };

8. Route Handlers(APIルート)

// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const posts = await db.post.findMany();
  return NextResponse.json(posts);
}

export async function POST(request: NextRequest) {
  const body = await request.json();
  const post = await db.post.create({ data: body });
  return NextResponse.json(post, { status: 201 });
}
// app/api/posts/[id]/route.ts
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const post = await db.post.findUnique({ where: { id } });
  if (!post) return NextResponse.json({ error: '見つかりません' }, { status: 404 });
  return NextResponse.json(post);
}

export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const body = await req.json();
  const post = await db.post.update({ where: { id }, data: body });
  return NextResponse.json(post);
}

export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  await db.post.delete({ where: { id } });
  return new NextResponse(null, { status: 204 });
}

9. Server Actions

// app/actions/posts.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod';

const schema = z.object({
  title: z.string().min(1).max(100),
  content: z.string().min(10),
});

export type ActionState = { errors?: { title?: string[]; content?: string[] }; message?: string };

export async function createPost(prevState: ActionState, formData: FormData): Promise<ActionState> {
  const validated = schema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
  });

  if (!validated.success) {
    return { errors: validated.error.flatten().fieldErrors, message: '入力エラー' };
  }

  await db.post.create({ data: validated.data });
  revalidatePath('/posts');
  redirect('/posts');
}
// フォームでの使用
'use client';
import { useActionState } from 'react';
import { createPost, type ActionState } from '@/app/actions/posts';

export default function NewPostPage() {
  const [state, formAction, isPending] = useActionState(createPost, {});
  return (
    <form action={formAction}>
      <input name="title" required />
      {state.errors?.title && <p>{state.errors.title[0]}</p>}
      <textarea name="content" required />
      {state.errors?.content && <p>{state.errors.content[0]}</p>}
      <button disabled={isPending}>{isPending ? '投稿中...' : '投稿する'}</button>
    </form>
  );
}

10. 画像最適化

import Image from 'next/image';

// ローカル画像(priority は LCP 要素に設定)
<Image src="/images/hero.jpg" alt="ヒーロー" width={1200} height={630} priority />

// リモート画像
<Image src="https://example.com/photo.jpg" alt="外部画像" width={800} height={600} />

// fill モード(親要素に relative 必要)
<div className="relative w-full h-96">
  <Image src="/banner.jpg" alt="バナー" fill style={{ objectFit: 'cover' }}
    sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" />
</div>
// next.config.js
const nextConfig = {
  images: {
    remotePatterns: [{ protocol: 'https', hostname: 'example.com' }],
    formats: ['image/avif', 'image/webp'],
  },
};

11. フォント最適化

import { Inter, Noto_Sans_JP } from 'next/font/google';

const inter = Inter({ subsets: ['latin'], display: 'swap', variable: '--font-inter' });
const notoSansJP = Noto_Sans_JP({
  subsets: ['latin'], weight: ['400', '500', '700'],
  display: 'swap', variable: '--font-noto-sans-jp',
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ja" className={`${inter.variable} ${notoSansJP.variable}`}>
      <body>{children}</body>
    </html>
  );
}

12. メタデータ

// 静的メタデータ
import type { Metadata } from 'next';
export const metadata: Metadata = {
  title: { template: '%s | My App', default: 'My App' },
  description: '説明文',
  openGraph: { type: 'website', locale: 'ja_JP', images: [{ url: '/og.png', width: 1200, height: 630 }] },
  metadataBase: new URL('https://example.com'),
};

// 動的メタデータ
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);
  return { title: post.title, description: post.excerpt };
}

// OG画像の動的生成
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og';
export default async function OGImage({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return new ImageResponse(<div style={{ fontSize: 48, color: 'white' }}>{post.title}</div>);
}

// サイトマップ
// app/sitemap.ts
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getPosts();
  return [
    { url: 'https://example.com', lastModified: new Date(), priority: 1.0 },
    ...posts.map((p) => ({ url: `https://example.com/blog/${p.slug}`, lastModified: new Date(p.updatedAt) })),
  ];
}

13. ミドルウェア

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const isAuthenticated = request.cookies.has('auth-token');

  // 認証チェック
  if (pathname.startsWith('/dashboard') && !isAuthenticated) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  // セキュリティヘッダー
  const response = NextResponse.next();
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');

  return response;
}

export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*', '/login'],
};

14. 設定ファイル

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  reactStrictMode: true,
  poweredByHeader: false,
  compress: true,

  images: {
    remotePatterns: [{ protocol: 'https', hostname: 'example.com' }],
    formats: ['image/avif', 'image/webp'],
  },

  async redirects() {
    return [{ source: '/old', destination: '/new', permanent: true }];
  },

  async rewrites() {
    return {
      afterFiles: [{ source: '/api/proxy/:path*', destination: 'https://backend.example.com/:path*' }],
    };
  },

  experimental: {
    turbo: {},
    ppr: true,
    serverActions: { bodySizeLimit: '2mb' },
  },
};

export default nextConfig;

環境変数: NEXT_PUBLIC_ プレフィックスはクライアント側で使用可能。


15. パフォーマンス最適化

// 動的インポート
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), { ssr: false });

// スクリプト最適化
import Script from 'next/script';
<Script src="https://www.googletagmanager.com/gtag/js" strategy="afterInteractive" />
<Script src="https://cdn.example.com/widget.js" strategy="lazyOnload" />

// Web Vitals
'use client';
import { useReportWebVitals } from 'next/web-vitals';
export function WebVitals() {
  useReportWebVitals((metric) => console.log(`${metric.name}: ${metric.value}`));
  return null;
}
カテゴリ推奨事項
コンポーネントサーバーコンポーネントを優先
画像next/image を使用
フォントnext/font を使用
スクリプトnext/script を使用
ルーティングnext/link を使用
データキャッシュ戦略を適切に設定
バンドル動的インポートの活用
レンダリングSuspense でストリーミング

16. デプロイメント

16.1 Vercel

npm install -g vercel && vercel --prod

16.2 Docker

FROM node:20-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]

next.config.jsoutput: 'standalone' を設定。

16.3 静的エクスポート

const nextConfig = { output: 'export', images: { unoptimized: true }, trailingSlash: true };

制約:SSR不可、Middleware不可、ISR不可。


17. まとめ

17.1 Next.js の強み

  • 開発体験: ファイルシステムルーティング、TypeScript ゼロコンフィグ、Turbopack
  • パフォーマンス: 4層キャッシュ、自動コード分割、画像・フォント最適化
  • 柔軟性: SSG/ISR/SSR/CSR をページ単位で選択
  • フルスタック: Route Handlers、Server Actions、ミドルウェア

17.2 技術選定の指針

プロジェクト種別推奨戦略
コーポレートサイトSSG + ISR
EC サイトISR + SSR
ブログ / ドキュメントSSG
ダッシュボードSSR + CSR
SaaSSSR + Server Actions

17.3 エコシステム

  • スタイリング: Tailwind CSS、CSS Modules
  • 状態管理: Zustand、Jotai
  • ORM: Prisma、Drizzle ORM
  • 認証: NextAuth.js、Clerk
  • CMS: Contentful、Sanity、microCMS
  • テスト: Vitest、Playwright
  • デプロイ: Vercel、Docker

17.4 今後の展望

  1. Turbopack のプロダクションビルド対応
  2. Partial Prerendering の安定化
  3. React Server Components の深化
  4. 'use cache' ディレクティブの拡充
  5. Edge Computing の拡張

本記事の情報は Next.js 16.2.2 時点のものです。最新情報は Next.js 公式ドキュメント をご参照ください。