🏂

Next.js App RouterとPages Routerで同じルートを共存させる方法

に公開

はじめに

カウンターワークスの山田です。フロントエンド開発を担当しています。

Next.jsをPages RouterからApp Routerへ移行するとき、「同じURLパターンを両方のルーターで処理したい」という状況に直面することがあります。

例えば、新しいコンテンツはApp Routerで、レガシーコンテンツはPages Routerで処理しつつ、
URLは /posts/[post_id] のままにしたい、というケースです。

問題

https://example.com/posts/102  → App Router(新デザイン)
https://example.com/posts/90   → Pages Router(旧デザイン)

「片方のルーターで見つからなければ、もう片方にフォールバックしてくれるのでは?」と思うかもしれません。残念ながら、そうはなりません。

同じルートが両方に存在する場合、App Routerが常に優先されます。 ルーター間の自動フォールバックは存在しません。

my-app/
├── app/
│   └── posts/
│       └── [post_id]/
│           └── page.tsx    # 常にこちらが優先
├── pages/
│   └── posts/
│       └── [post_id].tsx  # 到達しない

解決策

fallback rewritesdynamicParams = false を組み合わせることで解決できます。

Step 1: Pages Router側のルートをリネーム

まず、Pages Router側のルートをリネームして、直接の競合を避けます:

my-app/
├── app/
│   └── posts/
│       └── [post_id]/
│           └── page.tsx
├── pages/
│   └── posts-legacy/        # リネーム
│       └── [post_id].tsx

Step 2: fallback rewritesを設定

next.config.js でfallback rewritesを設定します。
fallback rewritesは、「どのルートにもマッチしなかった場合」にのみ発動する特殊なrewriteです。通常のrewriteは条件に合えば常に発動しますが、fallback rewritesは最後の砦として機能します。

// next.config.js
module.exports = {
  async rewrites() {
    return {
      fallback: [
        {
          source: "/posts/:post_id",
          destination: "/posts-legacy/:post_id",
        },
      ],
    };
  },
};

この設定は「/posts/:post_id がアプリ内のどこにも存在しない場合、/posts-legacy/:post_id を代わりに表示する」という意味になります。

Step 3: dynamicParams = false を設定

App Routerに [post_id] という動的ルートがあると、デフォルトではすべての /posts/xxx にマッチしてしまいます。つまり、/posts/90をリクエストしても、App Routerが該当ルートを処理対象と判断し、fallback rewritesは発動しません。

これを防ぐために、App Router側で処理するIDを明示的に指定し、それ以外は「存在しない」扱いにする必要があります:

// app/posts/[post_id]/page.tsx

// App Routerで処理するpost IDを明示的に指定
export async function generateStaticParams() {
  // 例: post 100~105のみApp Routerで処理
  const newPostIds = [100, 101, 102, 103, 104, 105];
  
  return newPostIds.map((id) => ({
    post_id: id.toString(),
  }));
}

// generateStaticParamsに含まれないIDは404扱いにする
// これによりfallback rewritesが発動する
export const dynamicParams = false;

dynamicParams = false を設定することで:

  • generateStaticParams で返したID → App Routerで処理
  • それ以外のID → 「存在しない」扱い → fallback rewriteが発動 → Pages Routerへ

実際のアプリケーションでは、このIDリストをAPIやデータベースから取得することが多いでしょう

export async function generateStaticParams() {
  const response = await fetch('https://api.example.com/new-posts');
  const posts = await response.json();
  // ...
}

処理の流れ

リクエスト: /posts/102

App Routerに存在する? → Yes(generateStaticParamsに含まれる)

App Routerでレンダリング ✓

---

リクエスト: /posts/90

App Routerに存在する? → No(dynamicParams = falseにより404扱い)

Fallback rewrites発動 → /posts-legacy/90 に内部リライト

Pages Routerでレンダリング ✓
(ブラウザのURLは /posts/90 のまま)

---

リクエスト: /posts/9999

App Routerに存在する? → No

Fallback rewrites発動 → /posts-legacy/9999

Pages Routerにも存在しない → 404ページ表示

別解: Proxyを使う方法

dynamicParams を使いたくない場合は、Proxyで制御することもできます。

// proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const LAST_LEGACY_POST_ID = 107;

export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const match = pathname.match(/^\/posts\/(\d+)$/);

  if (match) {
    const postId = parseInt(match[1] ?? "0", 10);
    
    if (postId <= LAST_LEGACY_POST_ID) {
      return NextResponse.rewrite(
        new URL(`/posts-legacy/${postId}`, request.url)
      );
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: '/posts/:path*',
};

このProxy(proxy.ts)はすべてのリクエストで実行されるため、毎回fetchするとパフォーマンスに影響が出る恐れがあり、境界値(LAST_LEGACY_POST_ID)をハードコードする必要がありますが dynamicParams を使わずに済みます。

まとめ

Next.jsでApp RouterとPages Routerを同じURLパターンで共存させるには:

  1. Pages Router側のルートをリネームする
  2. next.config.js でfallback rewritesを設定する
  3. App Router側で dynamicParams = false を設定する

特に3番目のステップを忘れると、動的ルートがすべてのリクエストをキャッチしてしまい、fallback rewritesが発動しません。

この方法を使えば、レガシーコンテンツを維持しながら、新しいコンテンツを段階的にApp Routerへ移行できます。

We are hiring!

カウンターワークスでは一緒に働く仲間を絶賛募集中です。
今後の更なる成長のためには圧倒的に仲間が不足しています。皆さまのご応募お待ちしております!

https://counterworks.co.jp/recruit/?utm_source=zenn&utm_medium=referral&utm_campaign=advent-calendar-2025&utm_content=17

参考

https://nextjs.org/docs/app/api-reference/config/next-config-js/rewrites
https://nextjs.org/docs/app/getting-started/proxy

COUNTERWORKS テックブログ

Discussion