📌

【Next.js】動的ルーティングで404エラーが発生する原因と正しい設定方法

に公開

はじめに

Next.jsアプリケーションの開発において、動的ルーティングを用いたページが開発環境 (next dev) では正常に表示されるにもかかわらず、本番環境 (next build & next start) では404エラーを返す事象に遭遇することがある。この備忘録は、その原因と解決策を整理するものである。

本稿執筆時の環境は以下の通りである。

  • OS: macOS Sequoia 15.6
  • Next.js: 14.2.3
  • Node.js: 20.12.2
  • 使用ルーター: Pages Router および App Router

原因

この問題の主な原因は、Next.jsの静的サイト生成 (SSG: Static Site Generation) の仕組みにある。

next dev コマンドで実行される開発サーバーは、リクエスト毎にページをレンダリングする。そのため、動的ルート(例: /posts/some-id)へアクセスがあった場合、その場でページが生成され、正常に表示される。

一方、next build コマンドで本番用にビルドする場合、SSGが適用される動的ルートページは、ビルド時にどのパスを静的HTMLとして生成するかをあらかじめ知る必要がある。この「事前に生成するパスのリスト」をNext.jsに提供するのが getStaticPaths 関数である。

getStaticPaths で定義されていないパスにアクセスした場合の挙動は、fallback オプションの値によって決定される。このオプションのデフォルト値が false であるため、ビルド時に生成されなかったパスへのアクセスはすべて404エラーとなる。これが、開発環境と本番環境で挙動が異なる根本的な原因である。

App Routerにおいても、generateStaticParams 関数と dynamicParams オプションが同様の役割を担っている。

解決策

解決策は、アプリケーションの要件に応じて選択する。ここではPages RouterとApp Routerそれぞれの場合について記述する。

Pages Router (pages ディレクトリ) を使用している場合

動的ルートファイル(例: pages/posts/[id].js)で getStaticPaths を用いて設定を行う。

解決策1: fallback オプションを変更する

ビルド時にすべてのパスを生成できない、またはユーザーがコンテンツを生成するなどしてパスが動的に増える可能性がある場合は、fallback オプションを true または 'blocking' に設定する。

  • fallback: 'blocking': 新しいパスへの初回アクセス時、サーバーサイドでページのHTML生成が完了するまでユーザーを待たせる。生成後は静的ファイルとしてキャッシュされる。SEO上有利とされる。
  • fallback: true: 新しいパスへの初回アクセス時、まずフォールバック用のページ(ローディング表示など)を即座に返し、バックグラウンドでページのHTML生成を行う。生成が完了すると、ページ内容が更新される。
fallback: 'blocking' の設定例
pages/posts/[id].js
export async function getStaticPaths() {
  // ここではビルド時に事前に生成するパスを空にするか、
  // よくアクセスされるページのみを定義する
  const paths = [];

  return {
    paths,
    fallback: 'blocking',
  };
}

export async function getStaticProps({ params }) {
  const res = await fetch(`https://api.example.com/posts/${params.id}`);
  const post = await res.json();

  if (!post) {
    return {
      notFound: true,
    };
  }

  return {
    props: { post },
    revalidate: 60, // ISR (Incremental Static Regeneration) を有効にする場合
  };
}

function PostPage({ post }) {
  // 'blocking' なのでローディング状態を自前で管理する必要はない
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  );
}

export default PostPage;
fallback: true の設定例

fallback: true を使用する場合、コンポーネント側でフォールバック中かどうかを判定し、ローディングUIを表示する必要がある。

pages/posts/[id].js
import { useRouter } from 'next/router';

// getStaticPaths, getStaticProps は 'blocking' の例と同様に記述
// fallback の値を true に変更する
export async function getStaticPaths() {
  return {
    paths: [],
    fallback: true,
  };
}
// ... getStaticProps

function PostPage({ post }) {
  const router = useRouter();

  // isFallbackがtrueの間、ローディングUIを表示する
  if (router.isFallback) {
    return <div>Loading...</div>;
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  );
}

export default PostPage;

解決策2: ビルド時にすべてのパスを静的に生成する

ブログ記事のように、存在するパスがすべて事前に確定している場合は、getStaticPaths ですべてのパスを生成するのが最も効率的である。

pages/posts/[id].js
export async function getStaticPaths() {
  // APIなどからすべての投稿データを取得
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();

  // 取得したデータからパスの配列を生成
  const paths = posts.map((post) => ({
    params: { id: post.id.toString() },
  }));

  return {
    paths,
    fallback: false, // 全てのパスを生成したので false のままで良い
  };
}

export async function getStaticProps({ params }) {
  // ...
}

// ... Component

App Router (app ディレクトリ) を使用している場合

App Routerでは、generateStaticParams 関数が getStaticPaths の役割を担う。

解決策1: generateStaticParams でパスを定義する

ビルド時に静的に生成したいパスを generateStaticParams で返す。

app/posts/[slug]/page.js
// ビルド時に静的に生成するパスを返す
export async function generateStaticParams() {
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();
 
  return posts.map((post) => ({
    slug: post.slug,
  }));
}

// ページコンポーネント
async function getPost(slug) {
  const res = await fetch(`https://api.example.com/posts/${slug}`);
  return res.json();
}

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

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  );
}

解決策2: 動的レンダリングを許可する

App Routerでは、デフォルトで dynamicParamstrue に設定されている。これは、generateStaticParams で生成されなかったパスにアクセスされた場合でも、オンデマンドでページを生成する挙動(fallback: 'blocking' に近い)を意味する。

もし404が発生する場合、page.jslayout.js、あるいはルートの layout.jsdynamicParamsfalse に設定されていないか確認する。

app/posts/[slug]/layout.js
// この設定があると、generateStaticParams で定義されていないパスは404になる
export const dynamicParams = false;

この設定が意図しないものであれば、削除するか true に変更することで、未知のパスでもページが生成されるようになる。

まとめ

Next.jsの動的ルーティングで発生する404エラーは、主にSSGにおけるビルド時の挙動と開発サーバーの挙動の差異に起因する。

  • Pages Router: getStaticPathsfallback オプション ('blocking' または true) を適切に設定することで、ビルド時に存在しなかったパスへのアクセスに対応できる。
  • App Router: デフォルトで動的レンダリングが有効 (dynamicParams: true)。404になる場合は dynamicParams の設定を確認する。

静的に生成するパスの数やページの性質に応じて、最適なデータフェッチ戦略を選択することが重要である。

参考ドキュメント

Discussion