Next.js

App Router

Next.js

ファイルベースルーティングとレイアウト

ファイル規約

app/ ディレクトリのファイル名と役割

structure.sh bash
app/
├── layout.tsx          # ルートレイアウト(必須)
├── page.tsx            # / ルート
├── loading.tsx         # ローディングUI(Suspense自動ラップ)
├── error.tsx           # エラーUI(Error Boundary自動ラップ)
├── not-found.tsx       # 404ページ
├── global-error.tsx    # ルートレイアウトのエラー

├── (marketing)/        # ルートグループ(URLに影響しない)
   ├── layout.tsx      # このグループ専用レイアウト
   ├── about/page.tsx  # /about
   └── blog/page.tsx   # /blog

├── dashboard/
   ├── layout.tsx      # /dashboard 共通レイアウト
   ├── page.tsx        # /dashboard
   ├── settings/
   └── page.tsx    # /dashboard/settings
   └── @modal/         # 並列ルート(Parallel Routes)
       └── page.tsx

├── [id]/               # 動的ルート
   └── page.tsx        # /123, /abc, ...
├── [...slug]/          # キャッチオールルート
├── [[...slug]]/        # オプショナルキャッチオール

└── api/
    └── users/
        └── route.ts    # API Route Handler

Layout と Page

レイアウトの入れ子とページコンポーネント

app/layout.tsx tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';

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

// メタデータ(静的)
export const metadata: Metadata = {
    title:       { default: 'MyApp', template: '%s | MyApp' },
    description: 'My Next.js application',
    openGraph:   { type: 'website', url: 'https://myapp.com' },
};

// ルートレイアウト(必須 - html, body タグを含む)
export default function RootLayout({
    children,
}: {
    children: React.ReactNode;
}) {
    return (
        <html lang="ja" className={inter.className}>
            <body>
                <Header />
                <main>{children}</main>
                <Footer />
            </body>
        </html>
    );
}
app/blog/[slug]/page.tsx tsx
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';

interface Props {
    params:      { slug: string };
    searchParams: { [key: string]: string | string[] };
}

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

// 静的パスの事前生成(SSG)
export async function generateStaticParams() {
    const posts = await getAllPosts();
    return posts.map(p => ({ slug: p.slug }));
}

// Page コンポーネント(Server Component がデフォルト)
export default async function PostPage({ params, searchParams }: Props) {
    const post = await getPost(params.slug);
    if (!post) notFound();  // not-found.tsx を表示

    return <article>{post.content}</article>;
}