I recently redesigned the Next.js Weekly issue page and ran into a specific routing headache.
I wanted to move all future newsletter issues to the App Router, but keep the archive of 100+ old issues running on the Pages Router. The catch was that I needed them to share the exact same URL pattern: /issues/[issue_id]
If you request a legacy issue, it should render via the Pages Router. If you request a new one, it should hit the App Router.
The Goal:
- https://nextjsweekly.com/issues/108 (new design → App Router)
- https://nextjsweekly.com/issues/90 (old design → Pages Router)
Migrating the old content wasn't really an option. The data retrieval for the archive relies heavily on getStaticProps and legacy logic that is fundamentally different from the server components approach I built for the redesign.
I'll save the full breakdown of how I built the new site for a future post. For now, I want to focus on this specific routing trick.
You might think you can implement this behavior just by adding the same path in both routers, assuming that if an issue ID doesn't exist in one router, it will fallback to the other before throwing a 404 error. For example, calling issue 1 using the path /issues/1 would first check if it exists in the App Router (it doesn't), then see if it exists in the Pages Router (it does), and finally render it.
However, the default behavior in Next.js is that when there are two equal routes in the App and Pages routers, the one in the App Router takes precedence. So, if you want to conditionally use the App or Pages router for the exact same path, you need to find a different approach.
my-app/
|-- app/
| |-- layout.tsx
| |-- issues/
| |-- [issue_id]/
| |-- page.tsx # <- this route will take precendence
|-- pages/
| |-- issues/
| |-- [issue_id].tsx
|-- next.config.mjs
|-- package.json
Solution 1
Since there is no way to have the exact same route active in both routers simultaneously, I had to rename one of them. I decided to change the one in the Pages Router from issues/[issue_id] to issues-page/[issue_id].
Inside the proxy.ts file (formerly known as middleware.ts), I check the issue_id in the route. Depending on a hardcoded issue ID, the middleware either lets the request through to the App Router or catches it and redirects it to the Pages Router route issues-page/. The trick here is that we use the NextResponse.rewrite() method to change how the path looks in the browser. This ensures the rendered page always appears to be on the clean /issues/[issue_id] route.
// proxy.ts
const LAST_PAGES_ROUTER_ISSUE_ID = 107;
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const match = pathname.match(/^\/issues\/(\d+)$/);
if (match) {
const issueId = parseInt(match[1] ?? "0", 10);
if (issueId <= LAST_PAGES_ROUTER_ISSUE_ID) {
return NextResponse.rewrite(
new URL(`/issues-page/${issueId}`, request.url)
);
}
}
return NextResponse.next();
}
Although this works, I wasn't happy with the solution due to the hardcoded LAST_PAGES_ROUTER_ISSUE_ID and the overall implementation style. Luckily, I found a much better way.
Solution 2
I knew my instinct to use rewrites was correct, so I looked up the Next.js docs related to rewrite configuration in next.config.mjs and found exactly what I was looking for. There are rewrite options for many use cases, but I was specifically interested in the fallback rewrite.
You can use this to check if a route exists in your Next.js app and, if not, fallback to a different route. In the docs example, they show how to fallback to an external URL if the path doesn't exist in your app. This is a great way to incrementally migrate to Next.js, and for me, it is the perfect way to bridge the gap between the Pages Router and the App Router.
// next.config.mjs
module.exports = {
async rewrites() {
return {
fallback: [
{
source: "/issues/:issue_id",
destination: `/issues-page/:issue_id`,
},
],
};
},
};
Basically, here is what this config does: it checks if the issue exists inside the App Router first. If not, it checks if it exists inside the Pages Router (via the renamed route). If it finds it there, it renders it under the original source path. If it's not in either, it shows a 404 page.
I love this solution. Objectively, it is much more elegant than the first attempt.
Thanks for reading! Hope this saves you the headache I had!
If this was useful, you might like my weekly Next.js newsletter. I share a short roundup of new releases, articles, tools, and packages. If you’re curious, you can subscribe here: https://nextjsweekly.com/

Top comments (0)