Next.js チートシート

プロジェクトセットアップ & 設定

プロジェクト作成

最新の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 → パラレルルートのフォールバックUI
  • app/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 UI
  • app/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でのデータフェッチ:

従来通り useEffectfetch や、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>
  );
}

注意点:

  • widthheight は、画像のアスペクト比を保つために重要です (fill を除く)。
  • 外部ドメインの画像を使用するには、next.config.jsimages.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テストなどに利用されます。

プロジェクトのルート (pagesapp と同じ階層) に 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.jsoutput: '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:
    • アプリケーションをコンテナ化して、Dockerをサポートする任意の環境 (Kubernetes, AWS ECS, etc.) にデプロイします。
    • 公式ドキュメントに Dockerfile の例があります。
  • サーバーレスプラットフォーム (AWS Lambda, Google Cloud Functionsなど):
    • アダプター (例: 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();
      // ...
    }
  • メタデータ (App Router):
    layout.jspage.jsmetadata オブジェクトをエクスポートするか、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 }) {
      // ...
    }

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です