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 はこれらのギャップを埋め、以下のような機能を包括的に提供する。
| 機能カテゴリ | 素の React | Next.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 Router | Pages Router |
|---|---|---|
| ディレクトリ | app/ | pages/ |
| デフォルトコンポーネント | Server Components | Client Components |
| レイアウト | ネストレイアウト(layout.tsx) | _app.tsx のみ |
| データフェッチング | async コンポーネント + fetch | getServerSideProps / getStaticProps |
| ローディング UI | loading.tsx(ストリーミング) | 手動実装 |
| エラーハンドリング | error.tsx(Error Boundary) | _error.tsx |
| API ルート | route.ts(Route Handlers) | pages/api/ |
| メタデータ | metadata オブジェクト / generateMetadata | next/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.tsx | 404 | ルートが見つからない場合の UI |
route.ts | API ルート | サーバーサイドの 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>© 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.js で output: '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 |
| SaaS | SSR + 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 今後の展望
- Turbopack のプロダクションビルド対応
- Partial Prerendering の安定化
- React Server Components の深化
'use cache'ディレクティブの拡充- Edge Computing の拡張
本記事の情報は Next.js 16.2.2 時点のものです。最新情報は Next.js 公式ドキュメント をご参照ください。