| 1 | import { NextRequest, NextResponse } from "next/server"; |
| 2 | import { locales } from "@/lib/i18n/config"; |
| 3 | import { detectLocaleFromHeaders } from "@/lib/i18n/detect"; |
| 4 | |
| 5 | const COOKIE = "NEXT_LOCALE"; |
| 6 | |
| 7 | const SECURITY_HEADERS: Record<string, string> = { |
| 8 | "X-Frame-Options": "DENY", |
| 9 | "X-Content-Type-Options": "nosniff", |
| 10 | "Referrer-Policy": "strict-origin-when-cross-origin", |
| 11 | "Permissions-Policy": "camera=(), microphone=(), geolocation=(), interest-cohort=()", |
| 12 | "Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload", |
| 13 | }; |
| 14 | |
| 15 | function applySecurityHeaders(res: NextResponse): NextResponse { |
| 16 | for (const [k, v] of Object.entries(SECURITY_HEADERS)) res.headers.set(k, v); |
| 17 | return res; |
| 18 | } |
| 19 | |
| 20 | export function middleware(req: NextRequest) { |
| 21 | const { pathname } = req.nextUrl; |
| 22 | |
| 23 | // Skip API routes, static files, _next, and the dot-less metadata route |
| 24 | // for the shared OG image (but still apply security headers). |
| 25 | if ( |
| 26 | pathname.startsWith("/api/") || |
| 27 | pathname.startsWith("/_next/") || |
| 28 | pathname === "/opengraph-image" || |
| 29 | pathname.includes(".") |
| 30 | ) { |
| 31 | return applySecurityHeaders(NextResponse.next()); |
| 32 | } |
| 33 | |
| 34 | // Check if locale is already in path |
| 35 | const seg = pathname.split("/")[1]; |
| 36 | if (locales.includes(seg as typeof locales[number])) { |
| 37 | const res = NextResponse.next(); |
| 38 | res.cookies.set(COOKIE, seg, { path: "/", maxAge: 60 * 60 * 24 * 365 }); |
| 39 | return applySecurityHeaders(res); |
| 40 | } |
| 41 | |
| 42 | // Redirect bare paths to the detected locale (deterministic: cookie, then |
| 43 | // Accept-Language full-tag/primary-subtag matching, then the default). |
| 44 | const locale = detectLocaleFromHeaders( |
| 45 | req.cookies.get(COOKIE)?.value, |
| 46 | req.headers.get("accept-language"), |
| 47 | ); |
| 48 | const url = req.nextUrl.clone(); |
| 49 | url.pathname = `/${locale}${pathname}`; |
| 50 | const res = NextResponse.redirect(url); |
| 51 | res.cookies.set(COOKIE, locale, { path: "/", maxAge: 60 * 60 * 24 * 365 }); |
| 52 | return applySecurityHeaders(res); |
| 53 | } |
| 54 | |
| 55 | export const config = { |
| 56 | // Match everything so security headers apply globally; the function |
| 57 | // bypasses redirect/locale logic for /_next, /api, and dotted paths. |
| 58 | matcher: ["/:path*"], |
| 59 | }; |
| 60 |