learning-website-nextjs1-2 Exercise 2: One App That Rewrites by Host
=====================================================================
The other deployment shape: ONE Next.js app answers for every site. A proxy file runs before every request,
reads the Host header, and rewrites the request to that site's folder inside the app. The visitor's
address does not change. (In Next.js 16 this file is called proxy.ts; earlier versions called it
middleware.ts, and the build output still labels it "Proxy (Middleware)".)
Save as apps/multisite/proxy.ts:
import { NextResponse, type NextRequest } from "next/server";
import { environment, siteForHost } from "@lw/sites";
/**
* Runs before every request. It reads the Host header, decides which site the request is for, and REWRITES the
* request to that site's folder inside this one app: languages.localhost/x is served by app/sites/languages/x,
* and the visitor's address does not change.
*/
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
// /sites/... is the app's internal layout. A visitor must never reach it by typing it.
if (pathname === "/sites" || pathname.startsWith("/sites/")) {
return new NextResponse("Not found", { status: 404 });
}
// Only the Host header is trusted. X-Forwarded-Host is sent by the client unless a proxy overwrites it, so it is ignored.
const target = siteForHost(request.headers.get("host"), environment());
if (target === null) {
return new NextResponse("Unknown site", { status: 404, headers: { "content-type": "text/plain" } });
}
const url = request.nextUrl.clone();
url.pathname = `/sites/${target}${pathname === "/" ? "" : pathname}`;
return NextResponse.rewrite(url);
}
export const config = {
// everything except Next's own files (the compiled scripts and styles) and the icon
matcher: ["/((?!_next/|favicon.ico).*)"],
};
Save as apps/multisite/app/sites/[site]/page.tsx:
import { notFound } from "next/navigation";
import { SITES, SITE_NAMES, environment, siteUrl, type SiteName } from "@lw/sites";
// every site (and the front page) is produced at build time; any other value is a 404
export const dynamicParams = false;
export function generateStaticParams() {
return [...SITE_NAMES, "portfolio"].map((site) => ({ site }));
}
export default async function SitePage({ params }: { params: Promise<{ site: string }> }) {
const { site } = await params;
const env = environment();
if (site === "portfolio") {
return (
);
}
if (!(site in SITES)) notFound();
const name = site as SiteName;
return (
{SITES[name].title}
Folders on this site: {SITES[name].folders.join(", ")}.
);
}
How it fits together: languages.localhost/ is rewritten to /sites/languages, which is a page that was
built in advance (generateStaticParams lists the nine values, and dynamicParams = false makes any other
value a 404). The other files (package.json, next.config.ts, tsconfig.json, app/layout.tsx) are those of
the Chapter 1 apps with the name multisite and port 3010.
Four decisions in proxy.ts, each tested below:
1. /sites/... is the app's INTERNAL layout, so a request that asks for it directly is a 404; otherwise
languages.localhost/sites/systems would show another site's page on the wrong host.
2. Only the Host header is used. X-Forwarded-Host is sent by whoever makes the request unless a proxy
overwrites it, so it is ignored (the same decision as in the Django project).
3. An unknown host is a 404 with no page, never a default site.
4. The matcher skips /_next/ (compiled scripts and styles) so those are served normally.
npm run typecheck -w @lw/multisite passes
npm run build -w @lw/multisite 11.2 s; 9 static pages (/sites/languages ... portfolio); "ƒ Proxy (Middleware)"
npm run start -w @lw/multisite on port 3010
Requests sent to the running app (status, then the page's heading):
languages.localhost (curl resolves the name itself) 200 Languages
Host: webdevelopment.localhost:3010 200 Web Development
Host: LANGUAGES.localhost (upper case) 200 Languages
Host: localhost 200 Philip's Learning Notes
Host: example.com 404
Host: evillanguages.localhost 404
languages host asking for /sites/systems 404
languages host asking for /sites 404
languages host, /nothing-here 404
X-Forwarded-Host: systems.localhost sent to languages 200 Languages (the header was ignored)
no Host header at all (HTTP/1.0) 404
a file under /_next/static/ 200 application/javascript
response header on a site page: x-middleware-rewrite: /sites/languages
A headless Chrome opened http://languages.localhost:3010/ with no special settings and showed the
Languages page: browsers resolve *.localhost to this machine themselves, so no hosts-file entries are needed.
What was NOT checked: nothing was run behind Apache; the dynamic (not statically generated) case, where
the rewritten page is produced per request, was not built; "next dev" was not used.
WHY THIS WORKS AS AN ANSWER
---------------------------
The table is the test: every row is a request that must behave in exactly one way, including the ones that
try to break the rule (a made-up host, the internal path, a forged forwarded host).