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>
82 lines
2.9 KiB
TypeScript
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/|.*\\..*).*)"],
|
|
};
|