🐕

Next.js (SPA) × Apache:深層階層のURLが表示されない問題の「真犯人」と解決策

に公開

Next.js (SPA) × Apache:深層階層のURLが表示されない問題の「真犯人」と解決策

Next.jsを output: 'export' でビルドし、Apacheサーバーにデプロイした際、トップページからは遷移できるのに、深い階層のURL(例: /manual/admin/getting-started/auth/)に直接アクセスしたりリロードしたりすると、正しくページが表示されないという問題に直面することがあります。

.htaccess で index.html へのフォールバック設定をしているのになぜ? その原因と、skill-tracker.jp の実例から得た解決策を共有します。


1. 発生していた問題の概要

Apache側で以下のような「ファイルがなければ index.html を返す」という標準的なSPA向け設定を行っていました。

# .htaccess
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ /index.html [L]

一見正しく見えますが、/manual/admin/getting-started/auth/ のような深い動的ルートに直接アクセスすると、画面が正しくレンダリングされない、あるいは意図しない表示になる問題が発生しました。


2. 原因調査:ビルド出力の中身が「リテラル」だった

調査の結果、Next.jsのビルド出力(out/ ディレクトリ)に原因があることがわかりました。

ビルド時のログと出力ファイルの確認

# ビルド時のルート確認
Route (pages)
├ ○ /manual/[role]/[section]/[slug]   # ← 動的ルート

生成されたファイルを find コマンドで確認すると、衝撃の事実が判明します。

$ find out/manual -name "*.html"
/out/manual/index.html
/out/manual/[role]/[section]/[slug]/index.html  # ← 実際のパスではなく「文字列」のまま!

原因の特定:
動的ルートを使用しているにもかかわらず、getStaticPaths が定義されていませんでした。Next.jsの静的エクスポートでは、ビルド時に実際のパラメータ(admin や auth など)を知る術がないため、ファイル名をブラケットで囲まれたリテラル(文字列)のまま書き出していたのです。


3. 問題発生のメカニズム

なぜリテラルだと表示されないのか、その流れを整理します。

  1. ユーザー: /manual/admin/auth/ に直接アクセス。
  2. Apache: サーバー上に該当する物理パスがないため、.htaccess により /index.html を返す。
  3. ブラウザ: ルートの index.html を受信し、Next.jsを起動。
  4. Next.jsルーター: 現在のURL /manual/admin/auth/ を解析しようとする。
  5. 致命的な問題: ビルド時にこのパス用のHTMLやJSの関連付けが生成されていないため、クライアントサイド・レンダリングが正常に開始できない。

4. 解決策:getStaticPaths による事前生成

解決策は、ビルド時に存在する可能性のあるすべてのパスを Next.js に教え、実際の物理ディレクトリとして書き出させることです。

[slug].tsx への追記例

import { GetStaticPaths, GetStaticProps } from 'next';

// 1. ビルド時に生成するパスをすべて指定する
export const getStaticPaths: GetStaticPaths = async () => {
  const paths = [];
  // 役割、セクション、スラッグの組み合わせを網羅(データソースから取得)
  for (const role of ["admin", "user"]) {
    for (const section of Object.keys(manualDetails[role])) {
      for (const slug of Object.keys(manualDetails[role][section])) {
        paths.push({ params: { role, section, slug } });
      }
    }
  }
  return { paths, fallback: false };
};

// 2. 各パスに必要なデータを渡す(空でも必須)
export const getStaticProps: GetStaticProps = async ({ params }) => {
  return { props: { ...params } };
};

export default function ManualDetailPage({ role, section, slug }) {
  // ページコンポーネントの処理
}


5. 修正後の結果

修正後に再度ビルドを行うと、出力結果が劇的に変わります。

# 修正後の出力例
├ ● /manual/[role]/[section]/[slug]
├   ├ /manual/admin/getting-started/auth
├   ├ /manual/admin/getting-started/profile
├   └ [+33 more paths]

# 物理ファイルも実際のパスで生成される!
$ find out/manual -name "*.html"
/out/manual/admin/getting-started/auth/index.html   # ✓ 正常

項目 修正前 修正後
生成ファイル数 1ファイル 36ファイル(全パス分)
パス形式 [role]/[section]/[slug] admin/getting-started/auth 等
直接アクセスの結果 表示されない(404 or バグ) 正しく表示される

まとめ:教訓

Next.js を Apache で SPA 運用する場合、.htaccess のフォールバック設定はあくまで「保険」です。

「動的ルート([])を使うなら、getStaticPaths で物理パスを書き出させる」。

これが、深層階層での表示トラブルを防ぎ、かつ各ページが物理的に存在することで検索エンジン(SEO)にも優しいサイトを作るための鉄則です。


いかがでしょうか。もしこの構成で「サイトマップ(sitemap.xml)も自動生成して、よりSEOを強化したい」などの要望があれば、追加で解説を準備できますよ!

Discussion