@@ -4,35 +4,62 @@ import { getContentSecurityPolicy } from '@/util/cspConfig';
44import { fallbackLng , languages } from './app/i18n/settings' ;
55import { GET_PRODUCTS_OLD_SLUGS } from './query' ;
66import { fetchData , getRedirectURL , getStrapiGraphqlURL } from './util' ;
7+ import { handleFormTypeRewrite } from './util/handleFormTypeRewrite' ;
8+ import { withSecurityHeaders } from './util/withSecurityHeaders' ; // ✅ Add this line
79import { GetProductsOldSlugsQuery } from '../gql/graphql' ;
810
911acceptLanguage . languages ( languages ) ;
1012
1113export const config = {
1214 matcher : [ '/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)' ] ,
13- // https://nextjs.org/docs/messages/edge-dynamic-code-evaluation
1415 unstable_allowDynamic : [ '**/node_modules/lodash.mergewith/index.js' ] ,
1516} ;
1617
1718const cookieName = 'i18next' ;
19+ /**
20+ * 🌐 Global Middleware
21+ *
22+ * This middleware handles localization, form rewrites, legacy URL redirects,
23+ * and security headers for all incoming requests.
24+ *
25+ * ⚠️ ORDER MATTERS – Execution is top-down and early returns stop processing.
26+ *
27+ * Key Responsibilities:
28+ * 1. ✅ Set security headers (CSP, Referrer-Policy, etc.) via `withSecurityHeaders`
29+ * 2. ✅ Skip unnecessary processing for static/icon/chrome-related assets
30+ * 3. ✅ Redirect to a locale-prefixed path if not present (e.g., /nl, /en)
31+ * 4. ✅ Rewrite `/form` or `/formulier` URLs to include `formType` query param
32+ * 5. ✅ Set language cookie from Referer if available
33+ * 6. ✅ Redirect legacy URLs (e.g., old slugs from CMS)
34+ * 7. ✅ Return a default response with all security headers applied
35+ *
36+ * ⚠️ CRITICAL NOTES:
37+ * - `NextResponse.redirect()` or `NextResponse.rewrite()` will immediately return a response.
38+ * 👉 Ensure critical logic like `handleFormTypeRewrite` runs BEFORE any potential redirect.
39+ *
40+ * - Always wrap your `NextResponse` with `withSecurityHeaders()` to ensure security headers are applied consistently.
41+ * This includes responses returned by `next()`, `rewrite()`, and `redirect()`.
42+ *
43+ * - Nonce generation is included to support strict CSP rules for inline styles/scripts.
44+ *
45+ * See `withSecurityHeaders.ts` and `handleFormTypeRewrite.ts` for supporting logic.
46+ */
1847
1948export async function middleware ( req : NextRequest ) {
20- let locale ;
21- if ( req . cookies . has ( cookieName ) ) locale = acceptLanguage . get ( req . cookies . get ( cookieName ) ?. value ) ;
22- if ( ! locale ) locale = acceptLanguage . get ( req . headers . get ( 'Accept-Language' ) ) ;
23- if ( ! locale ) locale = fallbackLng ;
49+ const locale =
50+ ( req . cookies . has ( cookieName ) && acceptLanguage . get ( req . cookies . get ( cookieName ) ?. value ) ) ||
51+ acceptLanguage . get ( req . headers . get ( 'Accept-Language' ) ) ||
52+ fallbackLng ;
2453
2554 const { data } = await fetchData < { data : GetProductsOldSlugsQuery } > ( {
2655 url : getStrapiGraphqlURL ( ) ,
2756 query : GET_PRODUCTS_OLD_SLUGS ,
28- variables : {
29- locale,
30- } ,
57+ variables : { locale } ,
3158 } ) ;
3259
3360 const nonce = Buffer . from ( crypto . randomUUID ( ) ) . toString ( 'base64' ) ;
34-
3561 const cspHeader = getContentSecurityPolicy ( { nonce, node_env : process . env . NODE_ENV } ) ;
62+
3663 const headers = new Headers ( req . headers ) ;
3764 headers . set ( 'X-Nonce' , nonce ) ;
3865
@@ -43,41 +70,46 @@ export async function middleware(req: NextRequest) {
4370 'Permissions-Policy' : 'geolocation=(self)' ,
4471 } ;
4572
46- if ( req . nextUrl . pathname . indexOf ( 'icon' ) > - 1 || req . nextUrl . pathname . indexOf ( 'chrome' ) > - 1 )
47- return NextResponse . next ( ) ;
73+ const pathname = req . nextUrl . pathname ;
74+
75+ // Skip icons and Chrome-specific routes
76+ if ( pathname . includes ( 'icon' ) || pathname . includes ( 'chrome' ) ) {
77+ return withSecurityHeaders ( NextResponse . next ( ) , responseHeaders ) ;
78+ }
79+
80+ // Redirect if locale is missing from path
81+ if ( ! languages . some ( ( loc ) => pathname . startsWith ( `/${ loc } ` ) ) && ! pathname . startsWith ( '/_next' ) ) {
82+ const redirectUrl = new URL ( `/${ locale } ${ pathname } ${ req . nextUrl . search } ` , req . url ) ;
83+ return withSecurityHeaders ( NextResponse . redirect ( redirectUrl ) , responseHeaders ) ;
84+ }
4885
49- // Redirect if lng in path is not supported
50- if (
51- ! languages . some ( ( loc ) => req . nextUrl . pathname . startsWith ( `/${ loc } ` ) ) &&
52- ! req . nextUrl . pathname . startsWith ( '/_next' )
53- ) {
54- return NextResponse . redirect ( new URL ( `/${ locale } ${ req . nextUrl . pathname } ${ req . nextUrl . search } ` , req . url ) ) ;
86+ // Handle /form or /formulier rewrite
87+ const formRewriteResponse = handleFormTypeRewrite ( req , headers , responseHeaders ) ;
88+ if ( formRewriteResponse ) {
89+ return withSecurityHeaders ( formRewriteResponse , responseHeaders ) ;
5590 }
5691
92+ // Set cookie based on referer
5793 if ( req . headers . has ( 'referer' ) ) {
58- const refererUrl = new URL ( req . headers . get ( 'referer' ) as any ) ;
94+ const refererUrl = new URL ( req . headers . get ( 'referer' ) as string ) ;
5995 const lngInReferer = languages . find ( ( l ) => refererUrl . pathname . startsWith ( `/${ l } ` ) ) ;
60- const response = NextResponse . next ( {
61- request : { headers } ,
62- headers : responseHeaders ,
63- } ) ;
96+ const response = withSecurityHeaders ( NextResponse . next ( { request : { headers } } ) , responseHeaders ) ;
6497 if ( lngInReferer ) response . cookies . set ( cookieName , lngInReferer ) ;
6598 return response ;
6699 }
67100
68- // Fetches the redirect URL for a given request if the current path matches any old slugs.
69- const url = getRedirectURL ( {
101+ // Redirect legacy slugs
102+ const redirectUrl = getRedirectURL ( {
70103 url : req . nextUrl . clone ( ) ,
71- currentPathname : req . nextUrl . pathname ,
104+ currentPathname : pathname ,
72105 data : data . products ?. data ,
73106 } ) ;
74- // Use 308 for permanent redirects to inform browsers and search engines that the resource has moved permanently.
75- if ( url && url . toString ( ) !== req . nextUrl . href ) return NextResponse . redirect ( url , 308 ) ;
76107
77- headers . set ( 'x-pathname' , req . nextUrl . pathname ) ;
108+ if ( redirectUrl && redirectUrl . toString ( ) !== req . nextUrl . href ) {
109+ return withSecurityHeaders ( NextResponse . redirect ( redirectUrl , 308 ) , responseHeaders ) ;
110+ }
78111
79- return NextResponse . next ( {
80- request : { headers } ,
81- headers : responseHeaders ,
82- } ) ;
112+ // Default: return regular response with headers
113+ headers . set ( 'x-pathname' , pathname ) ;
114+ return withSecurityHeaders ( NextResponse . next ( { request : { headers } } ) , responseHeaders ) ;
83115}
0 commit comments