プロジェクトセットアップ & 設定
プロジェクト作成
最新のNext.jsプロジェクトをインタラクティブにセットアップします。
npx create-next-app@latest
特定のテンプレートを使用する場合:
npx create-next-app@latest --example <example-name>
TypeScriptでプロジェクトを開始:
npx create-next-app@latest --typescript
開発サーバー起動
npm run dev
# または
yarn dev
# または
pnpm dev
ビルド
本番用のアプリケーションをビルドします。
npm run build
# または
yarn build
# または
pnpm build
本番サーバー起動
ビルドされたアプリケーションを起動します。
npm start
# または
yarn start
# または
pnpm start
next.config.js
の設定例
基本的な設定ファイルです。様々なオプションを追加できます。
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true, // React Strict Modeを有効化
swcMinify: true, // SWC Minifierを使用
images: { // 画像最適化の設定
domains: ['example.com'], // 外部ドメインの画像を許可
},
env: { // 環境変数 (クライアントサイドで利用可能)
CUSTOM_ENV_VAR: process.env.CUSTOM_ENV_VAR,
},
// 他にもリダイレクト、リライト、ヘッダー設定など多数
async redirects() {
return [
{
source: '/old-path',
destination: '/new-path',
permanent: true, // 308 Permanent Redirect
},
]
},
async rewrites() {
return [
{
source: '/api/proxy/:path*',
destination: 'https://api.example.com/:path*', // 外部APIへのリライト
},
]
},
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'X-Custom-Header',
value: 'my custom header value',
},
],
},
]
},
// App Router利用時の設定例
experimental: {
// appDir: true, // Next.js 13.4以降はデフォルトで有効
typedRoutes: true, // 型付きルートを有効化 (実験的)
},
}
module.exports = nextConfig
ルーティング
Next.jsはファイルシステムベースのルーティングを採用しています。
App Router (推奨)
app
ディレクトリ内にファイルやフォルダを作成してルートを定義します。
app/page.js
→/
app/about/page.js
→/about
app/blog/[slug]/page.js
→/blog/:slug
(例:/blog/my-post
)app/shop/[[...slug]]/page.js
→/shop/*
(オプショナルキャッチオールルート 例:/shop
,/shop/a
,/shop/a/b
)app/(marketing)/about/page.js
→/about
(ルートグループ、URLパスには影響しない)app/@auth/login/page.js
→ パラレルルート (同じレイアウト内で複数の独立したページを表示)app/default.js
→ パラレルルートのフォールバックUIapp/layout.js
→ ルートレイアウト (必須)app/template.js
→ テンプレート (再レンダリング時に状態を保持しないレイアウト)app/loading.js
→ ローディングUI (Suspense境界)app/error.js
→ エラーUI (Client Componentである必要あり)app/global-error.js
→ グローバルエラーUI (ルートレイアウトのエラーをキャッチ)app/not-found.js
→ Not Found UIapp/route.js
→ APIエンドポイント (旧pages/api
相当)
ナビゲーション (App Router):
import Link from 'next/link';
import { useRouter } from 'next/navigation'; // App Router用
function NavigationComponent() {
const router = useRouter();
const handleNavigate = () => {
router.push('/dashboard');
};
return (
<div>
<Link href="/about">About</Link>
<Link href={{ pathname: '/blog/[slug]', query: { slug: 'my-post' } }}>
My Post
</Link>
<button onClick={handleNavigate}>Go to Dashboard</button>
<button onClick={() => router.back()}>Back</button>
<button onClick={() => router.refresh()}>Refresh (Server Componentsを再フェッチ)</button>
</div>
);
}
ルートパラメータの取得 (App Router):
// app/blog/[slug]/page.js
export default function BlogPostPage({ params }) {
// params = { slug: '...' }
return <h1>Blog Post: {params.slug}</h1>;
}
// app/shop/[[...slug]]/page.js
export default function ShopPage({ params }) {
// params = { slug: ['a', 'b'] } for /shop/a/b
// params = {} for /shop
return <h1>Shop: {params.slug?.join('/') || 'Home'}</h1>;
}
検索パラメータの取得 (App Router):
// app/search/page.js
export default function SearchPage({ searchParams }) {
// searchParams = { q: 'keyword' } for /search?q=keyword
return <h1>Search results for: {searchParams.q}</h1>;
}
// Client Component内で取得する場合
'use client';
import { useSearchParams } from 'next/navigation';
function SearchComponent() {
const searchParams = useSearchParams();
const query = searchParams.get('q');
return <p>Current query: {query}</p>;
}
Pages Router
pages
ディレクトリ内にファイルやフォルダを作成してルートを定義します。
pages/index.js
→/
pages/about.js
→/about
pages/posts/[id].js
→/posts/:id
(例:/posts/123
)pages/docs/[...slug].js
→/docs/*
(キャッチオールルート 例:/docs/a/b
)pages/_app.js
→ カスタムApp (全ページ共通のレイアウト)pages/_document.js
→ カスタムDocument (<html>
,<head>
,<body>
タグのカスタマイズ)pages/404.js
→ カスタム404ページpages/500.js
→ カスタム500ページpages/api/hello.js
→/api/hello
(APIルート)
ナビゲーション (Pages Router):
import Link from 'next/link';
import { useRouter } from 'next/router'; // Pages Router用
function NavigationComponent() {
const router = useRouter();
const handleNavigate = () => {
router.push('/dashboard');
};
return (
<div>
<Link href="/about">About</Link>
<Link href="/posts/[id]" as="/posts/123">
Post 123
</Link>
<button onClick={handleNavigate}>Go to Dashboard</button>
<button onClick={() => router.back()}>Back</button>
<button onClick={() => router.reload()}>Reload</button>
</div>
);
}
ルート/検索パラメータの取得 (Pages Router):
import { useRouter } from 'next/router';
function PostPage() {
const router = useRouter();
const { id, q } = router.query; // /posts/[id]?q=keyword
// id は [id].js の場合
// q はクエリパラメータ
// [...slug].js の場合は router.query.slug は配列になる
return (
<div>
<h1>Post ID: {id}</h1>
{q && <p>Search Query: {q}</p>}
</div>
);
}
export default PostPage;
データフェッチング
App Router
Server Componentsでは、async/await
を直接使用してデータを取得できます。
// app/posts/page.js (Server Component)
async function getData() {
const res = await fetch('https://api.example.com/posts');
if (!res.ok) {
throw new Error('Failed to fetch data');
}
return res.json();
}
export default async function PostsPage() {
const posts = await getData();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
キャッシュ制御:
fetch
関数のオプションやルートセグメント設定でキャッシュ戦略を制御します。
// デフォルト: キャッシュ有効 (SSGに近い挙動)
fetch('https://...');
// キャッシュしない (SSRに近い挙動)
fetch('https://...', { cache: 'no-store' });
// 一定時間で再検証 (ISR)
fetch('https://...', { next: { revalidate: 60 } }); // 60秒後に再検証
// ルートセグメント全体の設定 (page.js や layout.js)
export const dynamic = 'auto' // デフォルト: キャッシュを試みる
// export const dynamic = 'force-dynamic' // SSR: キャッシュしない
// export const dynamic = 'error' // SSG: 動的な関数やキャッシュしないデータがあればエラー
// export const dynamic = 'force-static' // SSG: 動的な関数やキャッシュしないデータがあれば強制的に静的化 (失敗する可能性あり)
export const revalidate = 60; // ISR: 60秒
// generateStaticParams (SSGのパス生成)
// app/blog/[slug]/page.js
export async function generateStaticParams() {
const posts = await fetch('https://.../posts').then((res) => res.json());
return posts.map((post) => ({
slug: post.slug,
}));
}
Client Componentでのデータフェッチ:
従来通り useEffect
と fetch
や、SWR、React Queryなどのライブラリを使用します。
'use client';
import { useState, useEffect } from 'react';
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then((res) => res.json());
function UserProfile({ userId }) {
// useEffect を使う場合
const [data, setData] = useState(null);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => {
setData(data);
setLoading(false);
});
}, [userId]);
// SWR を使う場合
const { data: swrData, error } = useSWR(`/api/users/${userId}`, fetcher);
if (isLoading || !swrData) return <p>Loading...</p>;
if (error) return <p>Error loading data.</p>;
return (
<div>
<h1>{swrData.name}</h1>
<p>{swrData.email}</p>
</div>
);
}
Server Actions (フォーム送信やMutation):
Client Componentからサーバー側の関数を直接呼び出せます。
// app/actions.js
'use server'; // Server Actionを示すディレクティブ
export async function createPost(formData) {
const title = formData.get('title');
// ... データベースへの保存処理など
console.log('Creating post:', title);
// 必要に応じて revalidatePath('/') や redirect('/') を実行
}
// app/new-post/page.js (Client Componentである必要はないが、多くの場合フォームはClient Component)
'use client';
import { createPost } from '../actions';
import { useFormState, useFormStatus } from 'react-dom'; // React 18.3+
function SubmitButton() {
const { pending } = useFormStatus();
return <button type="submit" disabled={pending}>{pending ? '送信中...' : '作成'}</button>;
}
export default function NewPostPage() {
// useFormState を使う場合(オプション)
const initialState = { message: null };
const [state, formAction] = useFormState(createPost, initialState);
return (
// <form action={createPost}> // シンプルな場合
<form action={formAction}>
<label htmlFor="title">タイトル:</label>
<input type="text" id="title" name="title" required />
<SubmitButton />
{state?.message && <p>{state.message}</p>}
</form>
);
}
Pages Router
ページごとに特定のデータフェッチ関数を使用します。
関数 | 実行タイミング | 主な用途 | 備考 |
---|---|---|---|
getStaticProps |
ビルド時 | SSG (Static Site Generation) | revalidate オプションで ISR に対応。context オブジェクトでパラメータ (params ) を受け取れる。 |
getStaticPaths |
ビルド時 | 動的ルートのSSGパス生成 | getStaticProps と併用必須。fallback オプション (true , false , 'blocking' ) でビルド時未指定パスの挙動を制御。 |
getServerSideProps |
リクエスト毎 (サーバーサイド) | SSR (Server-Side Rendering) | 常に最新のデータが必要なページ。context オブジェクトでリクエスト情報 (req , res , query など) を受け取れる。 |
getStaticProps
の例 (SSG):
// pages/posts/[id].js
export async function getStaticPaths() {
// 事前に生成するパスを取得
const res = await fetch('https://.../posts');
const posts = await res.json();
const paths = posts.map((post) => ({
params: { id: post.id.toString() },
}));
return { paths, fallback: 'blocking' }; // blocking, true, false
}
export async function getStaticProps({ params }) {
const res = await fetch(`https://.../posts/${params.id}`);
const post = await res.json();
if (!post) {
return { notFound: true }; // 404ページを表示
}
return {
props: { post },
revalidate: 60, // ISR: 60秒ごとに再生成を試みる(オプション)
};
}
function PostPage({ post }) {
// ビルド時または再検証時に取得した post データを使用
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}
export default PostPage;
getServerSideProps
の例 (SSR):
// pages/profile.js
import nookies from 'nookies'; // クッキー操作ライブラリの例
export async function getServerSideProps(context) {
const cookies = nookies.get(context);
const token = cookies.token;
// トークンがない場合はログインページへリダイレクト
if (!token) {
return {
redirect: {
destination: '/login',
permanent: false,
},
};
}
// 認証されたユーザー情報を取得
const res = await fetch('https://.../me', {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) {
nookies.destroy(context, 'token'); // エラー時はクッキー削除
return { redirect: { destination: '/login', permanent: false } };
}
const userData = await res.json();
return {
props: { userData }, // ページコンポーネントにpropsとして渡される
};
}
function ProfilePage({ userData }) {
// リクエスト毎に取得した userData を使用
return (
<div>
<h1>Welcome, {userData.name}!</h1>
<p>Email: {userData.email}</p>
</div>
);
}
export default ProfilePage;
クライアントサイドフェッチ (Pages Router):
App RouterのClient Componentと同様に、useEffect
や SWR、React Queryを使用します。
コンポーネント (App Router)
App Routerでは、コンポーネントはデフォルトで Server Components です。
Server Components
- サーバーサイドでのみレンダリングされる。
- JavaScriptバンドルに含まれず、クライアントへの送信量が少ない。
- 直接データフェッチ (
async/await
) が可能。 - Node.jsのAPIやサーバーサイド専用ライブラリにアクセス可能 (データベース接続など)。
useState
,useEffect
などのフックや、ブラウザAPI (window
,document
) は使用不可。- イベントハンドラ (
onClick
など) は直接記述できない (Client Componentに渡す必要がある)。
// app/components/ServerInfo.js (Server Component - デフォルト)
import fs from 'fs'; // Node.js API の例
async function getServerData() {
// サーバーサイドでのみ実行される処理
const data = await fetch('https://api.example.com/server-data').then(res => res.json());
const fileContent = fs.readFileSync('/path/to/server/file.txt', 'utf8'); // 例
return { data, fileContent };
}
export default async function ServerInfo() {
const { data, fileContent } = await getServerData();
return (
<div>
<h3>Server Data:</h3>
<pre>{JSON.stringify(data, null, 2)}</pre>
<h3>File Content (from server):</h3>
<p>{fileContent}</p>
</div>
);
}
Client Components
- ファイルの先頭に
'use client';
ディレクティブを記述する。 - 従来のReactコンポーネントと同様に動作し、クライアントサイドでレンダリング・ハイドレーションされる。
useState
,useEffect
,useContext
などのフックを使用可能。- イベントハンドラ (
onClick
,onChange
など) を使用可能。 - ブラウザAPI (
window
,localStorage
など) にアクセス可能。 - データフェッチは
useEffect
やSWR/React Queryなどで行う。
// app/components/Counter.js (Client Component)
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button className="button is-primary" onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Server Components と Client Components の連携
- Server Components は Client Components をインポートして使用できる。
- Client Components は Server Components を直接インポートできない (propsとして渡すことは可能)。
- Server Components から Client Components へはシリアライズ可能な props (文字列、数値、オブジェクトなど、関数やDateオブジェクトは注意) を渡す。
- イベントハンドラなどのインタラクティブな要素は Client Component に実装する。
// app/page.js (Server Component)
import Counter from './components/Counter'; // Client Componentをインポート
import ServerInfo from './components/ServerInfo'; // Server Componentをインポート
export default async function HomePage() {
const initialCount = 5; // Server Componentで初期値を決定
return (
<main>
<h1 className="title">Welcome!</h1>
<section className="section">
<h2 className="subtitle">Client Component Example:</h2>
{/* Client Componentにpropsを渡す */}
<Counter initialValue={initialCount} />
</section>
<section className="section">
<h2 className="subtitle">Server Component Example:</h2>
<ServerInfo />
</section>
</main>
);
}
// app/components/Counter.js (修正: propsを受け取る)
'use client';
import { useState } from 'react';
export default function Counter({ initialValue = 0 }) { // propsを受け取る
const [count, setCount] = useState(initialValue);
return (
<div>
<p>Count: {count}</p>
<button className="button is-primary" onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
APIルート
サーバーサイドで実行されるAPIエンドポイントを作成します。
App Router (app/api/.../route.js
)
route.js
(または route.ts
) というファイル名を使用し、HTTPメソッドに対応する関数 (GET
, POST
, PUT
, DELETE
など) をエクスポートします。
// app/api/hello/route.js
import { NextResponse } from 'next/server';
// GETリクエストハンドラ
export async function GET(request) {
// requestオブジェクトから情報を取得可能
// const { searchParams } = new URL(request.url);
// const name = searchParams.get('name');
return NextResponse.json({ message: `Hello, world!` });
}
// POSTリクエストハンドラ
export async function POST(request) {
const body = await request.json(); // リクエストボディを取得
console.log(body);
return NextResponse.json({ received: body }, { status: 201 });
}
// 他のHTTPメソッド (PUT, DELETE, PATCH, HEAD, OPTIONS) も同様に定義可能
export async function PUT(request) {
// ...
return NextResponse.json({ message: 'Data updated' });
}
// 動的ルートの場合: app/api/users/[userId]/route.js
export async function GET(request, { params }) {
// params = { userId: '...' }
const userId = params.userId;
// ... ユーザーデータを取得 ...
return NextResponse.json({ id: userId, name: `User ${userId}` });
}
レスポンス:
NextResponse
オブジェクトを使用してJSON、テキスト、リダイレクトなどを返します。
import { NextResponse } from 'next/server';
import { redirect } from 'next/navigation'; // リダイレクト用
// JSONレスポンス
NextResponse.json({ data: '...' }, { status: 200, headers: { 'X-Custom': 'value' } });
// テキストレスポンス
new Response('Plain text response', { status: 200, headers: { 'Content-Type': 'text/plain' } });
// リダイレクト (Server ComponentやServer Actionと同様)
redirect('/new-location');
// クッキーの設定
const response = NextResponse.json({ success: true });
response.cookies.set('token', 'abc123', { path: '/', httpOnly: true, maxAge: 60 * 60 });
// response.cookies.delete('token'); // クッキー削除
return response;
Pages Router (pages/api/...
)
pages/api
ディレクトリ内にファイルを作成し、デフォルトエクスポートとしてリクエストハンドラ関数を定義します。
// pages/api/user.js
import type { NextApiRequest, NextApiResponse } from 'next';
type Data = {
name: string;
} | {
error: string;
};
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
if (req.method === 'GET') {
// GETリクエストの処理
const { id } = req.query; // クエリパラメータ取得 (例: /api/user?id=123)
res.status(200).json({ name: `User ${id || 'Default'}` });
} else if (req.method === 'POST') {
// POSTリクエストの処理
const body = req.body; // リクエストボディ取得 (事前にパースされている)
console.log(body);
res.status(201).json({ name: `Created user ${body.name}` });
} else {
// 対応していないメソッド
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).json({ error: `Method ${req.method} Not Allowed` });
}
}
リクエストオブジェクト (req
):
req.query
: クエリパラメータを含むオブジェクトreq.body
: パースされたリクエストボディ (Content-Type
に基づく)req.cookies
: クッキーを含むオブジェクトreq.method
: HTTPメソッド ('GET'
,'POST'
など)req.headers
: ヘッダー情報
レスポンスオブジェクト (res
):
res.status(code)
: HTTPステータスコードを設定res.json(body)
: JSONレスポンスを送信res.send(body)
: テキスト、Bufferなどのレスポンスを送信res.redirect([status,] url)
: リダイレクトres.setHeader(name, value)
: レスポンスヘッダーを設定res.end()
: レスポンスを終了
ミドルウェア連携:
APIルートの前段で共通処理 (認証、ロギングなど) を行いたい場合は、Next.js Middleware (下記参照) や、next-connect
のようなライブラリを利用できます。
スタイリング
Next.jsは様々なスタイリング手法に対応しています。
-
グローバルCSS:
pages/_app.js
(Pages Router) または ルートのlayout.js
(App Router) でグローバルなCSSファイル (例:styles/globals.css
) をインポートします。// pages/_app.js または app/layout.js import '../styles/globals.css'; // パスは適宜調整 export default function App({ Component, pageProps }) { // ... return <Component {...pageProps} />; }
-
CSS Modules:
ファイル名を*.module.css
(または.scss
/.sass
) とし、コンポーネントにインポートして使用します。クラス名は自動的に一意になります。// components/Button.module.css .button { padding: 10px 20px; background-color: blue; color: white; border: none; border-radius: 5px; } .button:hover { background-color: darkblue; } // components/Button.js import styles from './Button.module.css'; export default function Button({ children }) { return <button className={styles.button}>{children}</button>; }
-
Sass/SCSS:
sass
パッケージをインストール (npm install sass
) すれば、.scss
や.sass
ファイルを直接インポートできます (CSS Modulesとしても利用可能)。 -
Tailwind CSS:
非常に人気のあるユーティリティファーストのCSSフレームワーク。 公式ドキュメントに従ってセットアップします (create-next-app
でも選択可能)。<!-- 例: Tailwindを使用した場合 --> <button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"> Click Me </button>
-
CSS-in-JS ライブラリ:
Styled Components, Emotion など。App Routerでの利用には追加設定が必要な場合があります (特にServer Componentsとの連携)。- App Router + Emotion/Styled Components: Client Component内で使用し、公式ガイドに従ってレジストリ設定を行う必要があります。
// Emotion の例 (Client Component内で) 'use client'; import styled from '@emotion/styled'; const StyledButton = styled.button` padding: 10px 15px; background-color: ${(props) => (props.primary ? 'palevioletred' : 'white')}; color: ${(props) => (props.primary ? 'white' : 'palevioletred')}; border: 2px solid palevioletred; border-radius: 3px; `; export default function MyComponent() { return ( <div> <StyledButton>Normal Button</StyledButton> <StyledButton primary>Primary Button</StyledButton> </div> ); }
画像最適化
next/image
コンポーネントを使用して、画像を自動的に最適化します。
- 最新フォーマット (WebPなど) への自動変換
- リサイズと最適化
- CLS (Cumulative Layout Shift) の防止
- 遅延読み込み (Lazy Loading)
import Image from 'next/image';
import localImage from '../public/my-image.jpg'; // publicディレクトリ内の画像
function MyImageComponent() {
return (
<div>
{/* ローカル画像 */}
<Image
src={localImage}
alt="説明"
width={500} // レイアウトシフト防止に必須 (fill以外)
height={300} // レイアウトシフト防止に必須 (fill以外)
// quality={75} // 画質 (デフォルト 75)
// priority // 重要度の高い画像 (LCP候補など) は優先読み込み
// placeholder="blur" // ブラーアッププレースホルダー (ローカル画像では自動)
// blurDataURL="..." // カスタムプレースホルダー
/>
{/* 外部画像 (next.config.jsでのドメイン許可が必要) */}
<Image
src="https://images.example.com/remote-image.png"
alt="外部画像の説明"
width={600}
height={400}
/>
{/* 親要素に合わせたレスポンシブ画像 (親要素に position: relative が必要) */}
<div style={{ position: 'relative', width: '100%', height: '300px' }}>
<Image
src="/another-image.webp" // publicディレクトリ内の画像は / から始まるパス
alt="レスポンシブ画像"
fill // 親要素にフィット
style={{ objectFit: 'cover' }} // CSSの object-fit を適用
/>
</div>
</div>
);
}
注意点:
width
とheight
は、画像のアスペクト比を保つために重要です (fill
を除く)。- 外部ドメインの画像を使用するには、
next.config.js
のimages.domains
またはimages.remotePatterns
で設定が必要です。 - SVGファイルは通常、
next/image
を使わず、直接<img>
タグやReactコンポーネントとしてインポートして使用します。
// next.config.js での外部ドメイン設定例
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
port: '',
pathname: '/**', // または特定のパスパターン
},
{
protocol: 'https',
hostname: '**.example.com', // サブドメインを許可
},
],
// 旧: domains: ['images.unsplash.com', 'cdn.example.com'],
},
}
module.exports = nextConfig;
スクリプト最適化
next/script
コンポーネントを使用して、サードパーティスクリプトの読み込み戦略を制御します。
strategy 値 |
読み込みタイミング | 主な用途 |
---|---|---|
beforeInteractive |
ページのハイドレーション前 (サーバー、クライアント) | サイト全体で必須のスクリプト (Cookie同意管理など) |
afterInteractive (デフォルト) |
ページのハイドレーション後 (クライアント) | インタラクティブ性が求められるスクリプト (分析ツール、広告タグなど) |
lazyOnload |
ブラウザのアイドル時 (クライアント) | バックグラウンドで実行される優先度の低いスクリプト (チャットウィジェットなど) |
worker (実験的) |
Web Worker内 (クライアント) | メインスレッドをブロックしたくない重い処理 (オフロード可能なもの) |
import Script from 'next/script';
function MyPage() {
return (
<div>
{/* Google Analytics (例) */}
<Script
src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"
strategy="afterInteractive"
/>
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_MEASUREMENT_ID');
`}
</Script>
{/* 優先度の低いチャットウィジェット (例) */}
<Script
src="https://widget.example.com/chat.js"
strategy="lazyOnload"
/>
{/* ページ読み込み前に必須のスクリプト (例) */}
<Script
src="/path/to/critical-script.js"
strategy="beforeInteractive"
/>
</div>
);
}
onLoad
, onError
, onReady
コールバック:
スクリプトの読み込み完了時やエラー時に処理を実行できます (strategy="beforeInteractive"
では onLoad
, onError
は使えません)。onReady
はスクリプトがロードされ、コンポーネントがマウントされた後に実行されます。
'use client'; // コールバックはクライアントサイドで実行されるため
import Script from 'next/script';
import { useState } from 'react';
function ScriptComponent() {
const [isLoaded, setIsLoaded] = useState(false);
return (
<Script
src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY"
strategy="lazyOnload"
onLoad={() => {
console.log('Google Maps script loaded!');
setIsLoaded(true);
// ここでマップを初期化するなどの処理
}}
onError={(e) => {
console.error('Failed to load Google Maps script:', e);
}}
// onReady={() => { console.log('Script ready'); }}
/>
// {isLoaded && <div>Map Placeholder</div>}
);
}
環境変数
Next.jsは .env
ファイルによる環境変数管理をサポートしています。
.env
: 全環境でのデフォルト値。.env.local
: ローカル環境での上書き。Gitで追跡しないこと。.env.development
: 開発環境 (next dev
) での値。.env.production
: 本番環境 (next start
) での値。.env.test
: テスト環境 (NODE_ENV=test
) での値。
優先順位: .env.{environment}.local
> .env.local
(test
除く) > .env.{environment}
> .env
サーバーサイドでのアクセス
Node.js 環境 (getServerSideProps
, APIルート, Server Components, getStaticProps
, ミドルウェアなど) では、process.env.YOUR_VARIABLE_NAME
で直接アクセスできます。
// .env.local
DATABASE_URL="postgresql://user:password@host:port/db"
SECRET_KEY="my-secret"
// pages/api/data.js や app/api/data/route.js など
export default async function handler(req, res) { // Pages Routerの場合
// export async function GET(request) { // App Routerの場合
const dbUrl = process.env.DATABASE_URL;
const secret = process.env.SECRET_KEY;
// ... dbUrlやsecretを使って処理 ...
// return NextResponse.json({ message: 'Accessed server env vars' }); // App Router
res.status(200).json({ message: 'Accessed server env vars' }); // Pages Router
}
クライアントサイドでのアクセス
ブラウザで実行されるコード (通常のコンポーネント、Client Components) で環境変数を利用するには、変数名の先頭に NEXT_PUBLIC_
を付ける必要があります。
// .env.local
NEXT_PUBLIC_API_ENDPOINT="https://api.example.com"
NEXT_PUBLIC_GOOGLE_ANALYTICS_ID="UA-XXXXX-Y"
// components/ApiClient.js (Client Component や Pages Router のコンポーネント)
'use client'; // App Router の Client Component の場合
function ApiClient() {
const endpoint = process.env.NEXT_PUBLIC_API_ENDPOINT;
const gaId = process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_ID;
const fetchData = async () => {
const res = await fetch(`${endpoint}/items`);
const data = await res.json();
console.log(data);
};
return (
<div>
<p>API Endpoint: {endpoint}</p>
<p>GA ID: {gaId}</p>
<button onClick={fetchData}>Fetch Data</button>
</div>
);
}
注意: NEXT_PUBLIC_
が付いた変数は、ビルド時にJavaScriptバンドルに埋め込まれます。機密情報 (APIキー、シークレットなど) には絶対に使用しないでください。
next.config.js
内の env
プロパティでもクライアントサイドで利用可能な変数を定義できますが、NEXT_PUBLIC_
プレフィックスの方が推奨されます。
ミドルウェア
リクエストが完了する前にコードを実行できます。認証、リダイレクト、ヘッダーの書き換え、A/Bテストなどに利用されます。
プロジェクトのルート (pages
や app
と同じ階層) に middleware.js
(または middleware.ts
) ファイルを作成します。
// middleware.js
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// 例1: 特定パスへのアクセスを認証済みユーザーに限定
const pathname = request.nextUrl.pathname;
const isAuthenticated = request.cookies.has('auth_token'); // クッキーで認証状態を確認
if (pathname.startsWith('/dashboard') && !isAuthenticated) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('from', pathname); // 元のパスをクエリに追加
return NextResponse.redirect(loginUrl);
}
// 例2: 国に基づいたリダイレクト
const country = request.geo?.country?.toLowerCase() || 'us';
if (country === 'jp' && pathname === '/') {
return NextResponse.redirect(new URL('/jp', request.url));
}
// 例3: リクエストヘッダーの追加
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-custom-header', 'middleware-value');
// 例4: レスポンスヘッダーの書き換え
const response = NextResponse.next({
request: { // ヘッダーを追加・変更したリクエストを渡す
headers: requestHeaders,
},
});
response.headers.set('x-middleware-ran', 'true');
// 処理を続行
return response; // または NextResponse.next()
}
// Middlewareを実行するパスを指定 (正規表現、文字列配列など)
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
// 特定のパスのみに適用する場合
// '/dashboard/:path*',
// '/about',
],
};
特徴:
- Edge Runtime で実行されるため、Node.js API の一部は利用できません (
fs
など)。 NextRequest
オブジェクトでリクエスト情報 (URL, ヘッダー, クッキー, geo情報など) にアクセスできます。NextResponse
オブジェクトを使って、リダイレクト (NextResponse.redirect
)、書き換え (NextResponse.rewrite
)、ヘッダー操作、レスポンスの生成が可能です。NextResponse.next()
を返すと、リクエスト処理が続行されます。config
オブジェクトのmatcher
で、ミドルウェアを適用するパスを指定します。
デプロイ
Next.jsアプリケーションのデプロイ方法はいくつかあります。
-
Vercel (推奨): Next.js の開発元であり、最もシームレスなデプロイ体験を提供します。
- Gitリポジトリ (GitHub, GitLab, Bitbucket) と連携して自動デプロイ。
- グローバルCDN、サーバーレス関数、画像最適化などを自動設定。
- 無料プランあり。
- コマンドラインツール:
vercel deploy
-
Node.js サーバー:
npm run build
でビルドし、npm start
で起動するNode.jsサーバーを自分でホストします。- AWS EC2, Google Cloud Run, DigitalOcean App Platform, Heroku などのプラットフォーム。
- インフラ管理 (スケーリング、ロギング、監視など) が必要。
-
静的エクスポート (Static HTML Export):
next.config.js
にoutput: 'export'
を設定し、npm run build
を実行すると、out
ディレクトリに静的なHTML/CSS/JSファイルが生成されます。- 画像最適化 (
next/image
) の一部機能、APIルート、ミドルウェア、ISR、SSRなどの動的な機能は利用できません。 - CDN (Netlify, Cloudflare Pages, AWS S3 + CloudFront など) に簡単にデプロイできます。
// next.config.js /** @type {import('next').NextConfig} */ const nextConfig = { output: 'export', // 注意: output: 'export' を使う場合、next/image のデフォルトローダーは使えない // images: { unoptimized: true }, // 必要に応じて設定 }; module.exports = nextConfig;
# package.json の build スクリプトを変更する場合 (推奨) # "build": "next build" npm run build # out ディレクトリが生成される
- アプリケーションをコンテナ化して、Dockerをサポートする任意の環境 (Kubernetes, AWS ECS, etc.) にデプロイします。
- 公式ドキュメントに Dockerfile の例があります。
- アダプター (例: Serverless Framework, AWS CDK) を使用してデプロイすることが可能です。設定が複雑になる場合があります。
- Vercel や Netlify は内部的にサーバーレスアーキテクチャを利用しています。
デプロイ先を選ぶ際は、アプリケーションの要件 (動的機能の有無、スケーラビリティ、コスト、管理の手間など) を考慮してください。
その他の便利な機能
-
フォント最適化 (
next/font
):
Google Fontsやローカルフォントを効率的に読み込み、CLSを防ぎ、プライバシーを向上させます。ビルド時にフォントファイルを自己ホストします。// app/layout.js (App Router) import { Inter, Noto_Sans_JP } from 'next/font/google'; const inter = Inter({ subsets: ['latin'], variable: '--font-inter' }); const notoSansJP = Noto_Sans_JP({ subsets: ['latin'], // 'latin', 'cyrillic' etc. 'japanese' は現在非推奨 weight: ['400', '700'], display: 'swap', variable: '--font-noto-sans-jp', }); export default function RootLayout({ children }) { return ( <html lang="ja" className={`${inter.variable} ${notoSansJP.variable}`}> <body>{children}</body> </html> ); } // CSS で使用する場合 // html { font-family: var(--font-inter), sans-serif; } // .japanese-text { font-family: var(--font-noto-sans-jp), sans-serif; }
// pages/_app.js (Pages Router) import { Inter } from 'next/font/google'; const inter = Inter({ subsets: ['latin'] }); export default function App({ Component, pageProps }) { return ( <main className={inter.className}> {/* classNameを直接適用 */} <Component {...pageProps} /> </main> ); }
-
絶対インポートとパスエイリアス:
jsconfig.json
またはtsconfig.json
でパスエイリアスを設定し、深い階層からのインポートを簡潔にします。// tsconfig.json または jsconfig.json { "compilerOptions": { "baseUrl": ".", "paths": { "@/components/*": ["components/*"], "@/lib/*": ["lib/*"], "@/styles/*": ["styles/*"] // App Router を使っている場合 // "@/app/*": ["app/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] }
// 使用例 import MyButton from '@/components/MyButton'; import { utilFunction } from '@/lib/utils';
-
ドラフトモード (Draft Mode):
ヘッドレスCMSなどから下書きコンテンツをプレビューするための機能。特定のCookieを設定することで、getStaticProps
や App Router のキャッシュをバイパスし、リクエスト時に最新データを取得します。- Pages Router:
res.setPreviewData({})
/res.clearPreviewData()
- App Router:
draftMode().enable()
/draftMode().disable()
(cookies()
を介して) - APIルートで有効化/無効化エンドポイントを作成するのが一般的です。
// app/api/draft/enable/route.js (App Router 例) import { draftMode } from 'next/headers'; import { redirect } from 'next/navigation'; export async function GET(request: Request) { draftMode().enable(); // 下書きモード有効後にリダイレクトしたいパスへ redirect('/'); }
// app/page.js (App Router 例) import { draftMode } from 'next/headers'; async function getData() { const { isEnabled } = draftMode(); const url = isEnabled ? '...' : '...'; // 下書き用/公開用エンドポイントを切り替え const res = await fetch(url, { // 下書きモード有効時はキャッシュしない cache: isEnabled ? 'no-store' : 'default', }); return res.json(); } export default async function Page() { const data = await getData(); // ... }
- Pages Router:
-
メタデータ (App Router):
layout.js
やpage.js
でmetadata
オブジェクトをエクスポートするか、generateMetadata
関数を使用して動的にメタデータ (<title>
,<meta>
など) を設定できます。// app/layout.js または app/page.js import type { Metadata } from 'next'; // 静的メタデータ export const metadata: Metadata = { title: 'サイトタイトル', description: 'サイトの説明', openGraph: { title: 'OGPタイトル', description: 'OGP説明', images: ['/og-image.png'], }, }; // 動的メタデータ (例: app/products/[id]/page.js) export async function generateMetadata({ params }): Promise<Metadata> { const product = await fetch(`https://.../products/${params.id}`).then((res) => res.json()); return { title: product.name, description: product.description, }; } export default function Page({ children }) { // ... }