How to Fix: Rewrites not working with Next 15 and React 19
Next.js 15 rewrites can appear broken with React 19 when the request is being handled by the wrong routing layer. In this case, navigating to /docs does not resolve through the expected rewrite because the application is using the App Router behavior in Next 15, where filesystem routes, route segments, and dev-server matching can take precedence in ways that differ from older setups. The fix is usually not in the link itself, but in how the rewrite is declared, where the destination points, and whether the destination is actually resolvable by the current router.
Table of Contents
Understanding the Root Cause
This bug typically happens because rewrites in Next.js are request-matching rules, not client-side aliases. In Next 15, especially with the App Router and React 19, navigation through <Link> may trigger route resolution paths that expose configuration mistakes more clearly than in previous versions.
Here is the technical reason: when you click a link such as /docs, Next.js first tries to determine whether that pathname maps to an existing route, static asset, middleware result, or rewrite target. If your rewrite points to a destination that is invalid for the current router structure, or if the target route does not exist exactly where Next expects it, the rewrite will look like it is ignored.
Common causes in this specific scenario include:
- A rewrite source like /docs exists, but the destination route is not implemented in the correct router directory.
- The app mixes pages and app directory behavior and assumes rewrites resolve identically across both.
- The destination points to a route segment that is not publicly addressable.
- Development navigation via Link behaves differently from directly loading the URL in the browser because Next prefetches and resolves route metadata.
- A conflicting filesystem route, middleware rule, redirect, or basePath setting overrides the rewrite.
In practice, the most reliable fix is to ensure the rewrite target matches a real route handled by the same routing system, and to validate behavior with both client-side navigation and a hard refresh.
Step-by-Step Solution
Use the following process to make rewrites work correctly in Next.js 15.
1. Define the rewrite in next.config.js
Make sure your configuration returns a valid rewrite rule and that the destination points to a real route.
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
return [
{
source: '/docs',
destination: '/documentation',
},
]
},
}
module.exports = nextConfig
If you are using TypeScript config:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
async rewrites() {
return [
{
source: '/docs',
destination: '/documentation',
},
]
},
}
export default nextConfig
2. Create the actual destination route
If you are using the App Router, the destination must exist under app:
app/
documentation/
page.tsx
export default function DocumentationPage() {
return <h1>Documentation</h1>
}
If you are using the Pages Router, it must exist under pages:
pages/
documentation.tsx
export default function DocumentationPage() {
return <h1>Documentation</h1>
}
3. Link to the source path, not the destination
Your link should continue to point to the public URL:
import Link from 'next/link'
export default function Home() {
return (
<Link href="/docs">GO TO DOCS</Link>
)
}
This is correct because the rewrite should transparently map /docs to /documentation.
4. Avoid rewriting to unsupported internal paths
Do not rewrite to internal implementation details, private segments, or non-route files. The destination must be a route Next can render publicly.
Bad example:
{
source: '/docs',
destination: '/app/documentation/page',
}
Correct example:
{
source: '/docs',
destination: '/documentation',
}
5. Restart the dev server after changing rewrites
next.config.js changes are not always reflected safely during hot reload. Stop and restart development:
npm run dev
If the rule still seems broken, remove the build cache and start again:
rm -rf .next
npm run dev
6. Test both browser refresh and client-side navigation
Check these separately:
- Open /docs directly in the browser.
- Navigate to /docs using <Link>.
If one works and the other does not, the issue is usually related to route manifest resolution, prefetch behavior, or a conflicting route definition.
7. If using App Router, keep route ownership clear
Do not define the same logical page across both app and pages unless you have a very specific migration plan. Route ambiguity can make rewrites appear inconsistent.
app/
page.tsx
documentation/
page.tsx
# Avoid also defining:
pages/
documentation.tsx
8. Prefer redirects if you want the URL to change
A rewrite keeps the browser URL as /docs. If you actually want users to land on /documentation visibly, use a redirect instead.
const nextConfig = {
async redirects() {
return [
{
source: '/docs',
destination: '/documentation',
permanent: false,
},
]
},
}
module.exports = nextConfig
Recommended final structure
app/
page.tsx
documentation/
page.tsx
next.config.js
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
return [
{
source: '/docs',
destination: '/documentation',
},
]
},
}
module.exports = nextConfig
Common Edge Cases
- Trailing slash mismatch: If your app uses trailingSlash, then /docs and /docs/ may behave differently depending on configuration.
- basePath enabled: If basePath is configured, your effective public route may be different than expected.
- Middleware interference: A middleware matcher can rewrite, redirect, or block the request before your rewrite applies.
- Dynamic routes: A dynamic segment such as app/[slug]/page.tsx can unexpectedly capture paths and mask the rewrite issue.
- Route groups confusion: App Router route groups like (marketing) do not appear in the URL, but they still affect where the actual destination file must live.
- Conflicting redirects: If both a redirect and rewrite target the same source, the redirect usually wins, causing surprising results.
- Prefetch cache in dev: Client-side navigation may appear stale if the dev server cached earlier route metadata. Restarting the server often resolves this.
- External destinations: Rewrites to external origins have different behavior and are not a substitute for internal route fixes.
FAQ
Why does the rewrite fail only when I click a Link?
Because client-side navigation uses Next’s router, prefetch manifest, and route tree. If the destination is not represented correctly in the active router, navigation via <Link> can fail even when a direct request behaves differently.
Does React 19 itself break rewrites?
No. React 19 is usually not the root cause. The problem is more often exposed by Next.js 15 routing behavior, route resolution, or an invalid rewrite destination.
Should I use a rewrite or a redirect for /docs?
Use a rewrite if you want users to stay on /docs in the address bar while rendering another route internally. Use a redirect if you want the browser URL to change to the destination path.
The key takeaway is simple: in Next.js 15, a rewrite only works when the source matches correctly and the destination is a real, publicly renderable route in the router you are actually using. Once the destination route is aligned with the App Router or Pages Router, the /docs navigation starts behaving as expected.