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>
This commit is contained in:
+52
-24
@@ -1,8 +1,11 @@
|
||||
/**
|
||||
* Server request shell. Reads the incoming request once, classifies it via
|
||||
* the pure domain decision, and records a page view without touching the
|
||||
* response. All analytics happen server-side; no tracking script ships to
|
||||
* the browser.
|
||||
* 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
|
||||
@@ -12,7 +15,12 @@
|
||||
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() ?? "";
|
||||
@@ -22,32 +30,52 @@ const firstForwardedIp = (forwardedFor: string | null): string =>
|
||||
|
||||
const hostnameWithoutPort = (host: string): string => host.replace(/:\d+$/, "");
|
||||
|
||||
export default function proxy(request: NextRequest): NextResponse {
|
||||
const decision = decidePageview({
|
||||
method: request.method,
|
||||
secFetchDest: request.headers.get("sec-fetch-dest") ?? "",
|
||||
export function proxy(request: NextRequest): NextResponse {
|
||||
const route = routeRequest({
|
||||
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")),
|
||||
cookie: request.cookies.get(LOCALE_COOKIE)?.value,
|
||||
acceptLanguage: request.headers.get("accept-language"),
|
||||
});
|
||||
|
||||
if (decision.track) {
|
||||
recordPageviewToUmami(decision.facts);
|
||||
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");
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
// 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 = {
|
||||
matcher: [
|
||||
"/((?!_next/static|_next/image|.*\\.(?:png|jpe?g|gif|svg|webp|avif|ico|css|mjs|cjs|js|map|txt|xml|json|webmanifest|woff2?|ttf|otf|mp4|webm|mov|pdf|zip)$).*)",
|
||||
],
|
||||
// Pages only: not Next's own files, the error proxy, or files.
|
||||
matcher: ["/((?!_next/|errors/|.*\\..*).*)"],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user