App Router
Next.js
ファイルベースルーティングとレイアウト
ファイル規約
app/ ディレクトリのファイル名と役割
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 HandlerLayout と Page
レイアウトの入れ子とページコンポーネント
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>
);
}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>;
}