Merge feat/german-locale: German version, glow effects and search metadata
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

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:
2026-10-04 14:20:10 +02:00
co-authored by Claude Opus 5.5
81 changed files with 6319 additions and 1915 deletions
+52 -24
View File
@@ -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/|.*\\..*).*)"],
};