import createMiddleware from 'next-intl/middleware'; import { NextRequest, NextResponse } from 'next/server'; // Create the internationalization middleware const intlMiddleware = createMiddleware({ // A list of all locales that are supported locales: ['en', 'de'], // Used when no locale matches defaultLocale: 'en', }); export default function middleware(request: NextRequest) { const { method, url, headers } = request; const { pathname } = request.nextUrl; // Explicit bypass for infrastructure routes to avoid locale redirects/interception if ( pathname.startsWith('/stats') || pathname.startsWith('/errors') || pathname.startsWith('/health') || pathname.includes('/api/og') || pathname.includes('opengraph-image') || pathname.endsWith('sitemap.xml') || pathname.endsWith('manifest.webmanifest') ) { return NextResponse.next(); } // Build header object for logging const headerObj: Record = {}; headers.forEach((value, key) => { headerObj[key] = value; }); // Defensive URL correction for internal container leakage (0.0.0.0, klz-app, localhost) // This prevents hydration mismatches and host poisoning in generated links/metadata. const urlObj = new URL(url); const internalHosts = ['0.0.0.0', 'klz-app', 'localhost', '127.0.0.1']; let effectiveRequest = request; if (internalHosts.includes(urlObj.hostname)) { const proto = headers.get('x-forwarded-proto') || 'https'; // Prioritize x-forwarded-host (passed by Traefik) over the local Host header const hostHeader = headers.get('x-forwarded-host') || headers.get('host') || 'testing.klz-cables.com'; urlObj.protocol = proto; effectiveRequest = new NextRequest(urlObj, { headers: request.headers, method: request.method, body: request.body, }); if (process.env.NODE_ENV !== 'production' || !process.env.CI) { console.log( `🛡️ Proxy: Fixed internal URL leak: ${url} -> ${urlObj.toString()} | Proto: ${proto} | Host: ${hostHeader}`, ); } } try { // Apply internationalization middleware const response = intlMiddleware(effectiveRequest); // Upgrade 307 (Temporary Redirect) to 308 (Permanent Redirect) // This improves compatibility with scanners (Website Carbon, PageSpeed) and SEO. if (response.status === 307) { const location = response.headers.get('Location'); if (location) { const url = new URL(location, request.url); return Response.redirect(url, 308); } } // Allow iframe embedding from recorder domains const referer = headers.get('referer') || ''; const recorderDomains = ['recorder.localhost', 'recorder.mintel.me']; const isRecorderRequest = recorderDomains.some((domain) => referer.includes(domain)); if (isRecorderRequest) { response.headers.delete('x-frame-options'); response.headers.delete('content-security-policy'); response.headers.set('Access-Control-Allow-Origin', '*'); } return response; } catch (error) { console.error( `Request failed: method=${method} url=${url} headers=${JSON.stringify(headerObj)}`, error, ); throw error; } } export const config = { matcher: [ '/((?!api|_next/static|_next/image|_img|favicon.ico|manifest.webmanifest|.*\\.(?:svg|png|jpg|jpeg|gif|webp|pdf|txt|vcf|xml|webm|mp4|map)$).*)', '/(de|en)/:path*', '/(de|en)/:path*', ], };