/** * 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. * * 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 { decidePageview } from "@/src/domain/pageview-decision"; 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 default function proxy(request: NextRequest): NextResponse { const decision = decidePageview({ method: request.method, secFetchDest: request.headers.get("sec-fetch-dest") ?? "", 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 (decision.track) { recordPageviewToUmami(decision.facts); } return NextResponse.next(); } 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)$).*)", ], };