はじめに

Next.js の App Router では、特定の名前を持つファイルが特殊な役割を果たします。このガイドでは、ファイルシステム規約について、各ファイルの役割と実践的な使い方を学んでいきます。

ファイルシステム規約とは

Next.js App Router では、appディレクトリ内の特定のファイル名が予約されており、それぞれが特別な機能を提供します。これにより、ルーティング、レイアウト、エラーハンドリングなどが宣言的に実装できます。

主要なファイル規約一覧

ルーティングファイル

app/
├── layout.tsx          # レイアウトを定義
├── page.tsx           # ページコンポーネントを定義
├── loading.tsx        # ローディング UI を定義
├── error.tsx          # エラー UI を定義
├── global-error.tsx   # グローバルエラー UI を定義
├── not-found.tsx      # 404 ページを定義
├── template.tsx       # テンプレートを定義
├── default.js         # 並行ルートのフォールバック(代替UI)
└── route.tsx          # API エンドポイントを定義

メタデータファイル

app/
├── favicon.ico        # ファビコン
├── icon.(ico|jpg|jpeg|png|svg)  # アプリアイコン
├── icon.tsx          # 動的アイコン生成
├── apple-icon.(jpg|jpeg|png)    # Appleアイコン
├── apple-icon.tsx    # 動的Appleアイコン生成
├── opengraph-image.(jpg|jpeg|png|gif)  # OG画像
├── opengraph-image.tsx  # 動的OG画像生成
├── twitter-image.(jpg|jpeg|png|gif)    # Twitter画像
├── twitter-image.tsx    # 動的Twitter画像生成
├── manifest.json     # PWAマニフェスト
├── manifest.ts       # 動的マニフェスト生成
├── robots.txt        # クローラー制御
├── robots.ts         # 動的robots生成
├── sitemap.xml       # サイトマップ
└── sitemap.ts        # 動的サイトマップ生成

その他の設定ファイル

├── middleware.ts      # ミドルウェアを定義(appディレクトリ外)
└── app/
    └── page.tsx      # Route Segment Config をエクスポート可能

page.tsx - ページの定義

基本的な使い方

page.tsxは、そのディレクトリを公開可能な URL として定義します。このファイルがないディレクトリは、URL としてアクセスできません。
// app/about/page.tsx
export default function AboutPage() {
  return (
    <div>
      <h1>About ページ</h1>
      <p>このページは /about でアクセスできます</p>
    </div>
  );
}

Dynamic Route での使用

// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
  return <h1>記事: {params.slug}</h1>;
}

layout.tsx - 共有レイアウト

基本的な使い方

layout.tsxは、複数のページで共有される UI を定義します。子ルートに自動的に適用され、ページ遷移時も再レンダリングされません。
// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body>
        <header>
          <nav>ナビゲーション</nav>
        </header>
        <main>{children}</main>
        <footer>フッター</footer>
      </body>
    </html>
  );
}

ネストされたレイアウト

ネストされたレイアウトとは、親のレイアウトの中に子のレイアウトが入れ子になっている構造のことです。Next.js では、ディレクトリ階層に沿って自動的にレイアウトがネストされます。

レイアウトの継承構造

app/
├── layout.tsx          # ルートレイアウト(全ページ共通)
└── dashboard/
    ├── layout.tsx      # ダッシュボードレイアウト
    ├── page.tsx        # /dashboard ページ
    └── analytics/
        ├── layout.tsx  # 分析レイアウト
        └── page.tsx    # /dashboard/analytics ページ
この構造では、/dashboard/analytics ページは以下の順序でレイアウトが適用されます:
  1. ルートレイアウト (app/layout.tsx) - 全体の HTML 構造
  2. ダッシュボードレイアウト (app/dashboard/layout.tsx) - ダッシュボード共通 UI
  3. 分析レイアウト (app/dashboard/analytics/layout.tsx) - 分析セクション固有 UI
  4. ページコンテンツ (app/dashboard/analytics/page.tsx) - 実際のページ

実装例

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="dashboard">
      <aside className="sidebar">
        <nav>
          <h2>ダッシュボード</h2>
          <ul>
            <li>
              <a href="/dashboard">概要</a>
            </li>
            <li>
              <a href="/dashboard/analytics">分析</a>
            </li>
            <li>
              <a href="/dashboard/settings">設定</a>
            </li>
          </ul>
        </nav>
      </aside>
      <div className="content">
        {/* ここに子レイアウトやページが挿入される */}
        {children}
      </div>
    </div>
  );
}
// app/dashboard/analytics/layout.tsx
export default function AnalyticsLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="analytics-section">
      <header>
        <h1>データ分析</h1>
        <div className="filter-bar">{/* フィルター UI */}</div>
      </header>
      <main>
        {/* ここに分析ページのコンテンツが挿入される */}
        {children}
      </main>
    </div>
  );
}

レンダリング結果

/dashboard/analytics にアクセスした場合の HTML 構造:
<!-- app/layout.tsx の内容 -->
<html>
  <body>
    <header>グローバルナビゲーション</header>

    <!-- app/dashboard/layout.tsx の内容 -->
    <div class="dashboard">
      <aside class="sidebar">...</aside>
      <div class="content">
        <!-- app/dashboard/analytics/layout.tsx の内容 -->
        <div class="analytics-section">
          <header>データ分析</header>
          <main>
            <!-- app/dashboard/analytics/page.tsx の内容 -->
            <div>実際のページコンテンツ</div>
          </main>
        </div>
      </div>
    </div>

    <footer>グローバルフッター</footer>
  </body>
</html>

メリット

  1. 再利用性: 共通の UI を複数のページで共有
  2. 保守性: レイアウトの変更が該当するセクションのみに影響
  3. パフォーマンス: ページ遷移時にレイアウトは再レンダリングされない
  4. ユーザー体験: 状態を保持したままページ間を移動可能

loading.tsx - ローディング UI

基本的な使い方

loading.tsxは、ページやセグメントのロード中に表示される UI を定義します。React Suspense を使用して自動的に適用されます。
// app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="loading-container">
      <div className="spinner" />
      <p>記事を読み込んでいます...</p>
    </div>
  );
}

スケルトンスクリーンの実装

スケルトンスクリーンとは

スケルトンスクリーン(Skeleton Screen)とは、コンテンツの読み込み中に表示される仮のUIのことです。実際のコンテンツの構造やレイアウトを模倣したグレーや薄い色のボックスで構成され、以下のメリットがあります:
  • 体感速度の向上: 真っ白な画面や単純なスピナーよりも読み込みが早く感じられる
  • レイアウトシフトの防止: コンテンツが読み込まれた際のレイアウトのずれを防ぐ
  • ユーザー体験の向上: 何が読み込まれるかを事前に示すことで、ユーザーの不安を軽減

実装例

商品一覧ページのスケルトンスクリーンの例:
// app/products/loading.tsx
export default function ProductsLoading() {
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
      {/* 6個の商品カードのスケルトンを表示 */}
      {[...Array(6)].map((_, i) => (
        <div key={i} className="skeleton-card border rounded-lg p-4">
          {/* 商品画像のスケルトン */}
          <div className="skeleton-image w-full h-48 bg-gray-200 rounded mb-4 animate-pulse" />

          {/* 商品名のスケルトン */}
          <div className="skeleton-text h-4 bg-gray-200 rounded mb-2 animate-pulse" />

          {/* 価格のスケルトン(短め) */}
          <div className="skeleton-text short h-4 w-1/2 bg-gray-200 rounded animate-pulse" />
        </div>
      ))}
    </div>
  );
}

CSS例

/* スケルトンスクリーンのスタイル */
.skeleton-card {
  border: 1px solid #e5e5e5;
  border-radius: 8px;
  padding: 16px;
}

.skeleton-image {
  width: 100%;
  height: 192px;
  background-color: #f3f4f6;
  border-radius: 4px;
  margin-bottom: 16px;
}

.skeleton-text {
  height: 16px;
  background-color: #f3f4f6;
  border-radius: 4px;
  margin-bottom: 8px;
}

.skeleton-text.short {
  width: 50%;
}

/* アニメーション効果 */
@keyframes pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}

.animate-pulse {
  animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}

ベストプラクティス

  1. 実際のレイアウトに近づける: 最終的なコンテンツと同じ構造にする
  2. 適切な数の要素: 実際に表示される要素数と合わせる
  3. アニメーション: 微細なパルス効果で読み込み中であることを示す
  4. 一貫性: アプリケーション全体で統一されたスケルトンデザインを使用

error.tsx - エラーハンドリング

基本的な使い方

error.tsxは、エラーが発生した際のフォールバック UI を提供します。
フォールバック(Fallback)とは
何らかの問題が発生した時に代替として表示される UI のことです。通常の処理が失敗した場合に「代わりに」表示されるコンテンツを指します。
自動的に React Error Boundary でラップされます。
// app/error.tsx
"use client"; // Error components must be Client Components

import { useEffect } from "react";

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // エラーをログに記録
    console.error(error);
  }, [error]);

  return (
    <div className="error-container">
      <h2>エラーが発生しました</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>もう一度試す</button>
    </div>
  );
}

セグメント固有のエラー処理

// app/dashboard/error.tsx
"use client";

export default function DashboardError({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div className="dashboard-error">
      <h2>ダッシュボードでエラーが発生しました</h2>
      <button onClick={() => reset()}>ダッシュボードを再読み込み</button>
    </div>
  );
}

not-found.tsx - 404 ページ

基本的な使い方

not-found.tsxは、特定のルートセグメントが見つからない場合に表示されます。
// app/not-found.tsx
import Link from "next/link";

export default function NotFound() {
  return (
    <div className="not-found">
      <h2>404 - ページが見つかりません</h2>
      <p>お探しのページは存在しません</p>
      <Link href="/">ホームに戻る</Link>
    </div>
  );
}

notFound() 関数との連携

// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";

async function getPost(slug: string) {
  const post = await fetchPost(slug);
  if (!post) {
    notFound(); // not-found.tsx を表示
  }
  return post;
}

export default async function BlogPost({
  params,
}: {
  params: { slug: string };
}) {
  const post = await getPost(params.slug);
  return <article>{post.content}</article>;
}

template.tsx - 再マウントされるレイアウト

基本的な使い方

template.tsxは、layout.tsxと似ていますが、ページ間を移動する際(ナビゲーション時)に新しいインスタンスが作成されます。

ナビゲーション時とは

ナビゲーション時とは、ユーザーがページ間を移動する時のことです。具体的には以下のような場面を指します:
  • リンククリック: <Link><a> タグをクリックして別のページに移動
  • ブラウザ操作: 戻る・進むボタンを使用してページを移動
  • 直接アクセス: URLバーに直接URLを入力してページにアクセス
  • プログラム実行: router.push()router.replace() などを実行

layout.tsx と template.tsx の違い

両者の主な違いは、ページ遷移時のコンポーネントの扱いです:
ファイルページ遷移時の動作状態の保持使用場面
layout.tsxコンポーネントが再利用される状態が保持されるナビゲーション、サイドバーなど永続的なUI
template.tsx新しいインスタンスが作成される状態がリセットされるページ遷移アニメーション、毎回リセットしたい状態
// app/template.tsx
export default function Template({ children }: { children: React.ReactNode }) {
  return <div className="fade-in">{children}</div>;
}

アニメーション付きテンプレート

// app/dashboard/template.tsx
"use client";

import { motion } from "framer-motion";

export default function DashboardTemplate({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.3 }}
    >
      {children}
    </motion.div>
  );
}

route.tsx - API エンドポイント

基本的な使い方

route.tsxは、API エンドポイントを定義します。HTTP メソッドに対応する関数をエクスポートします。

API ルートの配置について

route.ts(または route.js)ファイルは app ディレクトリ内のどこにでも配置できますが、実際の動作は配置場所によって異なります:
配置場所URL動作推奨度
app/api/hello/route.ts/api/helloAPI エンドポイントとして動作推奨
app/hello/route.ts/helloAPI エンドポイントとして動作⚠️ 非推奨
重要な注意点
  • route.tspage.ts同じディレクトリに共存できません
  • app/hello/route.ts がある場合、app/hello/page.ts は作成できません
  • 慣例的に API ルートは app/api/ 配下に配置することが強く推奨されます

基本的な実装例

// app/api/hello/route.ts
import { NextResponse } from "next/server";

export async function GET() {
  return NextResponse.json({ message: "Hello from API" });
}

export async function POST(request: Request) {
  const data = await request.json();
  return NextResponse.json({
    message: "データを受信しました",
    data,
  });
}

api 配下以外に配置した場合の例

// app/webhook/route.ts (非推奨)
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  // この場合、URLは /webhook になります
  const data = await request.json();
  return NextResponse.json({ received: true });
}
この配置方法は技術的には動作しますが、以下の理由で推奨されません:
  • 混乱を招く: ページなのかAPIなのかが不明確
  • 保守性の問題: API とページが混在して管理が困難
  • チーム開発の問題: 他の開発者が理解しにくい
  • Next.js の慣例に反する: 公式ドキュメントでも api/ 配下を推奨

Dynamic Route での API

// app/api/users/[id]/route.ts
import { NextResponse } from "next/server";

export async function GET(
  request: Request,
  { params }: { params: { id: string } },
) {
  const user = await getUser(params.id);

  if (!user) {
    return NextResponse.json(
      { error: "ユーザーが見つかりません" },
      { status: 404 },
    );
  }

  return NextResponse.json(user);
}

middleware.ts - リクエスト処理

基本的な使い方

middleware.tsは、リクエストが完了する前にコードを実行します。
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  // 認証チェック
  const token = request.cookies.get("token");

  if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: "/dashboard/:path*",
};

default.js - 並行ルートのフォールバック

基本的な使い方

default.jsは、並行ルートで初回ロード時やリロード時にマッチするセグメントがない場合のフォールバック(代替コンテンツ)を提供します。
// app/@team/default.tsx
export default function TeamDefault() {
  return <div>チームを選択してください</div>;
}

global-error.tsx - グローバルエラーハンドリング

基本的な使い方

global-error.tsxは、ルートレイアウトでのエラーをキャッチします。production 環境でのみ有効です。
// app/global-error.tsx
"use client";

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <html>
      <body>
        <h2>Something went wrong!</h2>
        <button onClick={() => reset()}>Try again</button>
      </body>
    </html>
  );
}

メタデータファイル規約

メタデータとは

メタデータ(metadata)とは、「データについてのデータ」を意味します。Web ページにおけるメタデータは、そのページの内容を説明する情報で、主に以下の用途で使用されます:
  • 検索エンジン最適化(SEO): ページタイトル、説明文、キーワードなど
  • ソーシャルメディア共有: OGP(Open Graph Protocol)画像、Twitter Card 情報
  • ブラウザ表示: ファビコン、テーマカラー、ビューポート設定
  • アプリケーション設定: PWA マニフェスト、アプリアイコン

メタデータファイルとは

Next.js App Router では、特定の名前を持つファイルを配置するだけで、自動的にメタデータが生成・配信されます。これらのファイルは:
  1. 静的ファイル: 画像やテキストファイルを直接配置(例:favicon.icorobots.txt
  2. 動的生成ファイル: TypeScript/JavaScript で動的に生成(例:sitemap.tsopengraph-image.tsx
Next.js は、これらのファイルを自動的に認識し、適切な HTML タグの生成、キャッシュの最適化、配信を行います。

favicon, icon, apple-icon

アプリケーションのアイコンを設定します。
app/
├── favicon.ico         # ブラウザタブ用
├── icon.png           # 一般的なアイコン
└── apple-icon.png     # Apple デバイス用
動的に生成する場合:
// app/icon.tsx
import { ImageResponse } from "next/og";

export const runtime = "edge";

export const size = {
  width: 32,
  height: 32,
};
export const contentType = "image/png";

export default function Icon() {
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 24,
          background: "black",
          width: "100%",
          height: "100%",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          color: "white",
        }}
      >
        A
      </div>
    ),
    {
      ...size,
    }
  );
}

opengraph-image と twitter-image

ソーシャルメディアでの共有時に表示される画像を設定します。
app/
├── opengraph-image.png    # Open Graph 画像 (1200x630)
└── twitter-image.png      # Twitter 画像 (1200x600)
動的に生成する場合:
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from "next/og";

export const runtime = "edge";

export const alt = "ブログ記事のプレビュー";
export const size = {
  width: 1200,
  height: 630,
};
export const contentType = "image/png";

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

  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 128,
          background: "white",
          width: "100%",
          height: "100%",
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          justifyContent: "center",
        }}
      >
        <div style={{ fontSize: 64, marginTop: 40 }}>{post.title}</div>
      </div>
    ),
    {
      ...size,
    }
  );
}

manifest.json

PWA(Progressive Web App)の設定を定義します。
// app/manifest.json
{
  "name": "My Next.js Application",
  "short_name": "Next.js App",
  "description": "Next.jsで構築されたアプリケーション",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#000000",
  "icons": [
    {
      "src": "/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}
動的に生成する場合:
// app/manifest.ts
import { MetadataRoute } from "next";

export default function manifest(): MetadataRoute.Manifest {
  return {
    name: "Next.js PWA",
    short_name: "NextPWA",
    description: "Next.js Progressive Web App",
    start_url: "/",
    display: "standalone",
    background_color: "#fff",
    theme_color: "#fff",
    icons: [
      {
        src: "/favicon.ico",
        sizes: "any",
        type: "image/x-icon",
      },
    ],
  };
}

robots.txt

検索エンジンクローラーの動作を制御します。
// app/robots.txt
User-Agent: *
Allow: /
Disallow: /private/

Sitemap: https://example.com/sitemap.xml
動的に生成する場合:
// app/robots.ts
import { MetadataRoute } from "next";

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: "*",
      allow: "/",
      disallow: "/private/",
    },
    sitemap: "https://example.com/sitemap.xml",
  };
}

sitemap.xml

サイトマップを生成して SEO を向上させます。
// app/sitemap.ts
import { MetadataRoute } from "next";

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: "https://example.com",
      lastModified: new Date(),
      changeFrequency: "yearly",
      priority: 1,
    },
    {
      url: "https://example.com/about",
      lastModified: new Date(),
      changeFrequency: "monthly",
      priority: 0.8,
    },
    {
      url: "https://example.com/blog",
      lastModified: new Date(),
      changeFrequency: "weekly",
      priority: 0.5,
    },
  ];
}

Route Segment Config

ページの動作を制御する設定をエクスポートできます。

generateStaticParams

Dynamic Route の静的生成パラメータを定義します。
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getPosts();

  return posts.map((post) => ({
    slug: post.slug,
  }));
}

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

generateMetadata

ページのメタデータを動的に生成します。
// app/products/[id]/page.tsx
import { Metadata } from "next";

type Props = {
  params: { id: string };
  searchParams: { [key: string]: string | string[] | undefined };
};

export async function generateMetadata({
  params,
  searchParams,
}: Props): Promise<Metadata> {
  const id = params.id;
  const product = await fetch(`https://.../${id}`).then((res) => res.json());

  return {
    title: product.name,
    description: product.description,
    openGraph: {
      images: ["/some-specific-page-image.jpg", ...product.images],
    },
  };
}

export default function Page({ params, searchParams }: Props) {
  return <h1>Product {params.id}</h1>;
}

Route Segment Config オプション

// app/page.tsx

// 動的関数の強制
export const dynamic = "force-dynamic"; // デフォルト: 'auto'

// 動的パラメータの強制
export const dynamicParams = true; // デフォルト: true

// 再検証の頻度(秒)
export const revalidate = 60; // デフォルト: false

// データフェッチのキャッシュ
export const fetchCache = "auto"; // デフォルト: 'auto'

// ランタイムの指定
export const runtime = "nodejs"; // デフォルト: 'nodejs'

// 優先レンダリングの地域
export const preferredRegion = "auto"; // デフォルト: 'auto'

// CPU負荷の高い処理の最大実行時間
export const maxDuration = 5; // デフォルト: プランによる

実践例:完全なブログアプリケーション

すべてのファイル規約を使用した例:
app/
├── layout.tsx              # ルートレイアウト
├── page.tsx               # ホームページ
├── loading.tsx            # グローバルローディング
├── error.tsx              # グローバルエラー
├── global-error.tsx       # プロダクション用グローバルエラー
├── not-found.tsx          # 404ページ
├── favicon.ico            # ファビコン
├── icon.png              # アプリアイコン
├── apple-icon.png        # Appleデバイス用アイコン
├── opengraph-image.png   # OGP画像
├── twitter-image.png     # Twitter Card画像
├── manifest.json         # PWAマニフェスト
├── robots.ts             # 動的robots.txt
├── sitemap.ts            # 動的サイトマップ
├── api/
│   └── posts/
│       ├── route.ts       # 記事一覧API
│       └── [id]/
│           └── route.ts   # 記事詳細API
└── blog/
    ├── layout.tsx         # ブログレイアウト
    ├── page.tsx          # ブログ一覧
    ├── loading.tsx       # ブログローディング
    └── [slug]/
        ├── page.tsx      # 記事詳細
        ├── loading.tsx   # 記事ローディング
        ├── error.tsx     # 記事エラー
        └── opengraph-image.tsx  # 動的OG画像

完全な実装例

// app/sitemap.ts
import { MetadataRoute } from "next";
import { getPosts } from "@/lib/posts";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getPosts();
  const blogPosts = posts.map((post) => ({
    url: `https://example.com/blog/${post.slug}`,
    lastModified: post.updatedAt,
    changeFrequency: "weekly" as const,
    priority: 0.8,
  }));

  return [
    {
      url: "https://example.com",
      lastModified: new Date(),
      changeFrequency: "yearly",
      priority: 1,
    },
    {
      url: "https://example.com/blog",
      lastModified: new Date(),
      changeFrequency: "daily",
      priority: 0.9,
    },
    ...blogPosts,
  ];
}
// app/blog/[slug]/page.tsx
import { Metadata } from "next";
import { notFound } from "next/navigation";
import { getPost, getPosts } from "@/lib/posts";

// 静的パラメータの生成
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((post) => ({
    slug: post.slug,
  }));
}

// 動的メタデータの生成
export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  const post = await getPost(params.slug);

  if (!post) {
    return {
      title: "記事が見つかりません",
    };
  }

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: "article",
      publishedTime: post.publishedAt,
      authors: [post.author],
    },
  };
}

// 再検証の設定
export const revalidate = 3600; // 1時間ごとに再検証

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

  if (!post) {
    notFound();
  }

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

ベストプラクティス

1. 適切なファイルの選択

  • layout.tsx: ナビゲーションやフッターなど、永続的な UI に使用
  • template.tsx: ページ遷移アニメーションが必要な場合に使用
  • loading.tsx: データフェッチが必要なページで必ず実装
  • error.tsx: セグメント固有のエラー処理に使用
  • global-error.tsx: 本番環境でのルートレベルエラーに使用

2. エラーハンドリングの階層

// app/global-error.tsx - 本番環境のルートエラー
// app/error.tsx - アプリケーション全体のエラー
// app/dashboard/error.tsx - ダッシュボード固有のエラー
// app/dashboard/analytics/error.tsx - より具体的なエラー

3. メタデータの最適化

  • 静的ファイル: 変更頻度が低い場合は画像ファイルを直接配置
  • 動的生成: ページごとに異なる画像が必要な場合は .tsx で生成
  • キャッシング: Next.js が自動的にハッシュを付けてキャッシュ

4. SEO とアクセシビリティ

// 適切なサイトマップの生成
export default async function sitemap() {
  // 動的にすべてのページを含める
  const pages = await getAllPages();
  return pages.map((page) => ({
    url: page.url,
    lastModified: page.updatedAt,
    changeFrequency: page.changeFrequency,
    priority: page.priority,
  }));
}

5. パフォーマンスの最適化

// Route Segment Config で最適化
export const dynamic = "force-static"; // 静的生成を強制
export const revalidate = 3600; // 1時間ごとに再検証

// loading.tsx でスケルトンスクリーンを実装
export default function Loading() {
  // 実際のレイアウトに近いスケルトンを作成
  return <SkeletonLayout />;
}

6. ミドルウェアの設定

// middleware.ts で metadata ファイルを除外
export const config = {
  matcher: [
    /*
     * Match all request paths except:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico, sitemap.xml, robots.txt (metadata files)
     */
    "/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
  ],
};

まとめ

Next.js のファイルシステム規約により:
  1. 宣言的な実装: ファイル名で機能が決まる
  2. 自動的な適用: 設定不要で機能が有効化
  3. 段階的な拡張: 必要に応じてファイルを追加
  4. 優れた DX: 直感的で理解しやすい構造
これらの規約を活用することで、保守性が高く、ユーザー体験に優れたアプリケーションを構築できます。

参考リンク

ファイル規約

ルーティング

メタデータと SEO