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. 問題発生のメカニズム
なぜリテラルだと表示されないのか、その流れを整理します。
-
ユーザー:
/manual/admin/auth/に直接アクセス。 -
Apache: サーバー上に該当する物理パスがないため、
.htaccessにより/index.htmlを返す。 -
ブラウザ: ルートの
index.htmlを受信し、Next.jsを起動。 -
Next.jsルーター: 現在のURL
/manual/admin/auth/を解析しようとする。 - 致命的な問題: ビルド時にこのパス用の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