import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; const SUPPORTED_LOCALES = ["en", "de"] as const; type SupportedLocale = (typeof SUPPORTED_LOCALES)[number]; function pickLocaleFromHeader(acceptLanguage: string | null): SupportedLocale { if (!acceptLanguage) return "en"; const lower = acceptLanguage.toLowerCase(); // Very small parser: prefer de, then en if (lower.includes("de")) return "de"; if (lower.includes("en")) return "en"; return "en"; } function hasLocalePrefix(pathname: string): boolean { return SUPPORTED_LOCALES.some((l) => pathname === `/${l}` || pathname.startsWith(`/${l}/`)); } export function middleware(request: NextRequest) { const { pathname, search } = request.nextUrl; // If a locale-prefixed request hits a public asset path (e.g. /de/images/me.jpg), // redirect to the non-prefixed asset path. if (hasLocalePrefix(pathname)) { const rest = pathname.replace(/^\/(en|de)/, "") || "/"; if (rest.includes(".")) { const responseUrl = request.nextUrl.clone(); responseUrl.pathname = rest; const res = NextResponse.redirect(responseUrl); return addHeaders(request, res); } } // Do not locale-route public assets (anything with a dot), robots, sitemap, etc. if (pathname.includes(".")) { return addHeaders(request, NextResponse.next()); } // Keep admin + APIs unlocalized for simplicity const isAdminOrApi = pathname.startsWith("/api/") || pathname === "/api" || pathname.startsWith("/manage") || pathname.startsWith("/editor"); // Locale routing for public site pages const responseUrl = request.nextUrl.clone(); if (!isAdminOrApi) { if (hasLocalePrefix(pathname)) { // Persist locale preference const locale = pathname.split("/")[1] as SupportedLocale; const res = NextResponse.next(); res.cookies.set("NEXT_LOCALE", locale, { path: "/" }); // Continue below to add security headers // eslint-disable-next-line no-use-before-define return addHeaders(request, res); } // Redirect bare routes to locale-prefixed ones const preferred = pickLocaleFromHeader(request.headers.get("accept-language")); const redirectTarget = pathname === "/" ? `/${preferred}` : `/${preferred}${pathname}${search || ""}`; responseUrl.pathname = redirectTarget; const res = NextResponse.redirect(responseUrl); res.cookies.set("NEXT_LOCALE", preferred, { path: "/" }); // eslint-disable-next-line no-use-before-define return addHeaders(request, res); } // Fix for 421 Misdirected Request with Nginx Proxy Manager // Ensure proper host header handling for reverse proxy const hostname = request.headers.get("host") || request.headers.get("x-forwarded-host") || ""; // Add security headers to all responses const response = NextResponse.next(); return addHeaders(request, response, hostname); } function addHeaders(request: NextRequest, response: NextResponse, hostnameOverride?: string) { const hostname = hostnameOverride ?? request.headers.get("host") ?? request.headers.get("x-forwarded-host") ?? ""; // Set proper headers for Nginx Proxy Manager if (hostname) { response.headers.set("X-Forwarded-Host", hostname); response.headers.set( "X-Real-IP", request.headers.get("x-real-ip") || request.headers.get("x-forwarded-for") || "", ); } // Security headers (complementing next.config.ts headers) response.headers.set("X-DNS-Prefetch-Control", "on"); response.headers.set("X-Frame-Options", "DENY"); response.headers.set("X-Content-Type-Options", "nosniff"); response.headers.set("X-XSS-Protection", "1; mode=block"); response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin"); response.headers.set( "Permissions-Policy", "camera=(), microphone=(), geolocation=()", ); // Rate limiting headers for API routes if (request.nextUrl.pathname.startsWith("/api/")) { response.headers.set("X-RateLimit-Limit", "100"); response.headers.set("X-RateLimit-Remaining", "99"); } return response; } export const config = { matcher: [ /* * Match all request paths except for the ones starting with: * - api (all API routes) * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico (favicon file) */ "/((?!api|_next/static|_next/image|favicon.ico).*)", ], };