Test
For convenience I set up a test env to replicate live.
How to test locally
You can simulate the subdomain on localhost by setting a fake host that points to 127.0.0.1, then run your dev server and visit that fake host. Two ways:
Next.js dev server
- Edit your hosts file
- macOS or Linux - open /etc/hosts with sudo and add to the bottom of the file:
127.0.0.1 example.test
127.0.0.1 sub.example.test
Windows - open C:\Windows\System32\drivers\etc\hosts as Administrator and add the same two lines.
- Start your app locally npm run dev Next.js usually runs on http://localhost:3000.
- You can visit the fake host in your browser http://example.test:3000 or http://sub.example.test:3000
Cause
When rewrites() returns an array, on default the rules run after filesystem routes loads. Therefore, root files comes before rewrite rules and overwrites it. If a real route exists at / for example app/page.tsx or pages/index.tsx, it matches before the rewrite. Therefore if I had tried rewriting something like this {source: "/", destination: "/brand"} visiting example.com would display the normal file (root) page example.com. But {source: "/probe", destination: "/brand"}(/probe route doesn't exist on my route pages) if I visit example.com/probe it would render example.com/probe but it would show me the content of example.com/brand (good).
Minimal local sanity check
Add a throwaway rule to verify rewriting works without host conditions:
async rewrites() {
return { afterFiles: [{ source: "/probe", destination: "/brand" }] };
}
Visit http://localhost:3000/probe or http://example.test:3000/probe and you should see /brand. Then add the beforeFiles root rule and the host based catch all as shown above.
Solution
The solution is to use the beforeFiles() to rewrite the root route; it return an object from rewrites() and put the root rule in beforeFiles so it runs before filesystem routes. Also keep a catch all in afterFiles for subpaths. Read
// next.config.ts — final working config
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
async rewrites() {
return {
// 1) Root of sub.example.com goes to /brand
beforeFiles: [
{
source: "/",
has: [{ type: "host", value: "sub.example.com" }],
destination: "/brand",
},
],
// 2) Subpaths on sub.example.com map to /brand/*
afterFiles: [
{
source: "/:path*",
has: [{ type: "host", value: "sub.example.com" }],
destination: "/brand/:path*",
},
],
};
},
};
export default nextConfig;
Notes
- Path case matters on Linux. If your folder is app/Brand, use /Brand, not /brand.
- Rewrites keep the URL only if the upstream does not redirect. If you rewrite to an origin that issues a 301 or 302 the browser URL will change.
- You can also do similar with vercel.json config but I could not figure how to make vercel rewite for the host, but it worked for any other path.
Vercel case
This works as normal
{
"rewrites": [
{
"source": "/probe",
"destination": "/brand"
}
]
}
This I haven't figure out yet. But it works for anyother route that isn't the root. But I doubt that is good enought for you
{
"rewrites": [
{ "source": "/", "has": [{ "type": "host", "value": "sub.example.com" }], "destination": "/brand" },
{ "source": "/:path*", "has": [{ "type": "host", "value": "sub.example.com" }], "destination": "/brand/:path*" }
]
}
References