Next.js × Vercel でリダイレクトループにハマった話

この記事は、Claude Code を使ってポータルサイトを開発する中で実際に遭遇したトラブルの記録です。AIと一緒に開発していても、Vercel のエッジルーティングの挙動のようなインフラ寄りの問題は試行錯誤が必要でした。

やりたかったこと

ST.Lab のポータルサイト(Next.js)から、別の Vercel プロジェクトにデプロイしている KabuVisualTaskDeck を、あたかも同じサイトのサブパスであるかのように配信したかった。

  • senryakutaro.com/apps/kabuvisual/kabuvisual.vercel.app/
  • senryakutaro.com/apps/taskdeck/task-deck-ivory.vercel.app/apps/taskdeck

Next.js の rewrites を使えばシンプルに実現できるはず...だった。

課題1: リダイレクトループ

症状

/apps/kabuvisual/ にアクセスすると 308 Permanent Redirect が返り、リダイレクト先が自分自身(/apps/kabuvisual/)という無限ループが発生。

原因

next.config.ts に以下の設定が同居していた:

redirects: /apps/kabuvisual → /apps/kabuvisual/ (末尾スラッシュ追加)
rewrites:  /apps/kabuvisual/ → https://kabuvisual.vercel.app/

Next.js は redirects → rewrites の順に評価する。しかし Vercel のエッジでは skipTrailingSlashRedirect の設定が完全には効かず、/apps/kabuvisual/ に対しても 308 が発生してループになっていた。

解決策

skipTrailingSlashRedirect と手動の redirects を両方削除し、代わりに trailingSlash: true を設定。Next.js のビルトイン機能に末尾スラッシュの処理を任せた。

const nextConfig = {
  trailingSlash: true,
  async rewrites() { /* ... */ },
};

課題2: CSS が 404

症状

リダイレクトループを解消した後、KabuVisual のページは表示されるが CSS が適用されない。開発者ツールで確認すると、CSS のリクエストが 404 になっていた。

原因

KabuVisual の HTML は相対パスで CSS を参照していた:

<link rel="stylesheet" href="css/style.css">

ブラウザが /apps/kabuvisual(末尾スラッシュなし)にアクセスした場合、相対パスの基準ディレクトリは /apps/ になる。そのため CSS のリクエスト先が /apps/css/style.css になり、rewrite ルールにマッチしなかった。

/apps/kabuvisual/(末尾スラッシュあり)なら基準ディレクトリが /apps/kabuvisual/ になり、正しく /apps/kabuvisual/css/style.css にリクエストされる。

解決策

ポータル内の全リンクを末尾スラッシュ付きに統一:

// Before
<Link href="/apps/kabuvisual">
// After
<Link href="/apps/kabuvisual/">

課題3: TaskDeck だけループする

症状

KabuVisual は正常に表示されるようになったが、TaskDeck だけリダイレクトループが再発。

原因

ポータル側は trailingSlash: true なので /apps/taskdeck/ にリダイレクトする。しかし TaskDeck 側のアプリは trailingSlash が未設定(デフォルト = false)のため、逆に /apps/taskdeck//apps/taskdeck に 308 リダイレクトを返していた。

ポータル: /apps/taskdeck → /apps/taskdeck/ (301)
TaskDeck: /apps/taskdeck/ → /apps/taskdeck (308)
→ ループ

解決策

rewrite の destination から末尾スラッシュを削除し、TaskDeck 側が期待するパスに合わせた:

{ source: '/apps/taskdeck/', destination: 'https://task-deck-ivory.vercel.app/apps/taskdeck' }

学んだこと

  1. trailingSlash の設定はプロキシ先と揃える — プロキシ元とプロキシ先で trailing slash のポリシーが異なるとループする
  2. 相対パスと末尾スラッシュの関係を意識する/path/path/ では相対パスの解決先が変わる
  3. redirectsrewrites の同居は慎重に — 同じパスに対して redirect と rewrite を設定すると、Vercel のエッジルーティングとの相互作用でループする可能性がある
  4. curl で確認する — ブラウザはキャッシュやリダイレクトを自動追従するため、curl -sv でレスポンスヘッダを直接確認するのが確実
  5. Claude Code でもハマるものはハマる — AI が書いたコードでも、Vercel のエッジルーティングのような環境依存の挙動はデプロイして初めてわかる。「動かして、壊れて、直す」サイクルを高速に回せるのが Claude Code の強みだった