Files
mintel.me/apps/web/proxy.ts
T
mmintelandClaude Opus 5.5 9ed436330b
Build & Deploy / 🩺 Smoke Test (push) Failing after 2s
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🏗️ Build (push) Successful in 6m36s
Build & Deploy / 🚀 Deploy (push) Successful in 24s
Build & Deploy / 🔔 Notify (push) Successful in 2s
Merge feat/german-locale: German version, glow effects and search metadata
Brings the German main language (English under /en), the binary glow
effects with the refined drawings and calls to action, and the search
metadata onto the server-side analytics from v2.1.0.

Both sides had added apps/web/proxy.ts. It is now one request shell: it
picks the language and maps the address (routes.ts), and records the page
view server-side (pageview-decision.ts) for every request that is not a
redirect, using the public address the visitor asked for. The browser
analytics component and the /stats proxy route stay removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 14:20:10 +02:00

82 lines
2.9 KiB
TypeScript

/**
* Server request shell. Reads the incoming request once and does two things,
* both decided by pure functions in src/domain:
*
* 1. Picks the language of the request and maps the public address onto the
* app directory (routes.ts).
* 2. Records a page view server-side (pageview-decision.ts). No tracking
* script ships to the browser.
*
* Page views are browser navigations (`sec-fetch-dest: document`).
* Router-internal headers (RSC, next-router-prefetch) are stripped by the
* server before this shell runs and cannot be used here.
*/
import { NextResponse, type NextRequest } from "next/server";
import { recordPageviewToUmami } from "@/src/analytics/recordPageviewToUmami";
import { LOCALE_COOKIE } from "@/src/domain/locale";
import { decidePageview } from "@/src/domain/pageview-decision";
import { routeRequest } from "@/src/domain/routes";
const PERMANENT = 308;
const TEMPORARY = 307;
const primaryLanguage = (acceptLanguage: string | null): string =>
acceptLanguage?.split(",")[0]?.split(";")[0]?.trim() ?? "";
const firstForwardedIp = (forwardedFor: string | null): string =>
forwardedFor?.split(",")[0]?.trim() ?? "";
const hostnameWithoutPort = (host: string): string => host.replace(/:\d+$/, "");
export function proxy(request: NextRequest): NextResponse {
const route = routeRequest({
pathname: request.nextUrl.pathname,
cookie: request.cookies.get(LOCALE_COOKIE)?.value,
acceptLanguage: request.headers.get("accept-language"),
});
const url = request.nextUrl.clone();
url.pathname = route.to;
const response =
route.kind === "redirect"
? NextResponse.redirect(url, route.permanent ? PERMANENT : TEMPORARY)
: NextResponse.rewrite(url);
// The answer depends on the visitor's language: caches must keep one per language.
if (route.negotiated) {
response.headers.append("Vary", "Accept-Language, Cookie");
}
// A redirect is not a view: the request that follows it is counted instead.
if (route.kind !== "redirect") {
const pageview = decidePageview({
method: request.method,
secFetchDest: request.headers.get("sec-fetch-dest") ?? "",
// The public address the visitor asked for, not the internal one it maps to.
pathname: request.nextUrl.pathname,
search: request.nextUrl.search,
hostname: hostnameWithoutPort(
request.headers.get("x-forwarded-host") ??
request.headers.get("host") ??
"",
),
referrer: request.headers.get("referer") ?? "",
language: primaryLanguage(request.headers.get("accept-language")),
userAgent: request.headers.get("user-agent") ?? "",
clientIp: firstForwardedIp(request.headers.get("x-forwarded-for")),
});
if (pageview.track) {
recordPageviewToUmami(pageview.facts);
}
}
return response;
}
export const config = {
// Pages only: not Next's own files, the error proxy, or files.
matcher: ["/((?!_next/|errors/|.*\\..*).*)"],
};