1+ /**
2+ * Cloudflare Pages middleware to handle trailing slash redirects
3+ *
4+ * This middleware addresses the known Cloudflare Pages issue where URLs without
5+ * trailing slashes don't match redirect rules that include trailing slashes.
6+ */
7+
8+ export async function onRequest ( context ) {
9+ const { request, env, next } = context ;
10+ const url = new URL ( request . url ) ;
11+
12+ // Only process GET and HEAD requests
13+ if ( request . method !== 'GET' && request . method !== 'HEAD' ) {
14+ return next ( ) ;
15+ }
16+
17+ // Skip if the path already has a trailing slash
18+ if ( url . pathname . endsWith ( '/' ) ) {
19+ return next ( ) ;
20+ }
21+
22+ // Skip if the path has a file extension
23+ const hasFileExtension = / \. [ a - z A - Z 0 - 9 ] + $ / . test ( url . pathname ) ;
24+ if ( hasFileExtension ) {
25+ return next ( ) ;
26+ }
27+
28+ // Skip certain system paths
29+ const skipPaths = [ '/robots.txt' , '/sitemap.xml' , '/_redirects' , '/favicon.ico' ] ;
30+ if ( skipPaths . includes ( url . pathname ) ) {
31+ return next ( ) ;
32+ }
33+
34+ // Skip paths with dots that might not be file extensions
35+ // but could be version numbers or other valid path segments
36+ // Only skip if the dot is followed by common file extensions
37+ const commonExtensions = / \. ( h t m l | h t m | c s s | j s | j s o n | x m l | t x t | p d f | j p g | j p e g | p n g | g i f | s v g | i c o | w o f f | w o f f 2 | t t f | e o t ) $ / i;
38+ if ( commonExtensions . test ( url . pathname ) ) {
39+ return next ( ) ;
40+ }
41+
42+ // Create URL with trailing slash, preserving query and hash
43+ const urlWithSlash = new URL ( request . url ) ;
44+ urlWithSlash . pathname = url . pathname + '/' ;
45+
46+ // Create a new request with the trailing slash
47+ const requestWithSlash = new Request ( urlWithSlash . toString ( ) , {
48+ method : request . method ,
49+ headers : request . headers ,
50+ redirect : 'manual' // Important: don't follow redirects
51+ } ) ;
52+
53+ try {
54+ // Fetch with the trailing slash URL
55+ const response = await env . ASSETS . fetch ( requestWithSlash ) ;
56+
57+ // If we get a redirect response (301, 302, etc.), redirect to URL with slash
58+ if ( response . status >= 301 && response . status <= 308 ) {
59+ // The path with trailing slash matches a redirect rule
60+ // Redirect the browser to the URL with slash, which will then trigger the actual redirect
61+ // Preserve query parameters and hash
62+ const redirectUrl = new URL ( request . url ) ;
63+ redirectUrl . pathname = url . pathname + '/' ;
64+
65+ return new Response ( null , {
66+ status : 301 ,
67+ headers : {
68+ 'Location' : redirectUrl . pathname + redirectUrl . search + redirectUrl . hash ,
69+ 'Cache-Control' : 'public, max-age=3600'
70+ }
71+ } ) ;
72+ }
73+
74+ // If the response is successful (200), it means the trailing slash version exists
75+ // In this case, we should also redirect to maintain consistency
76+ if ( response . status === 200 ) {
77+ const redirectUrl = new URL ( request . url ) ;
78+ redirectUrl . pathname = url . pathname + '/' ;
79+
80+ return new Response ( null , {
81+ status : 301 ,
82+ headers : {
83+ 'Location' : redirectUrl . pathname + redirectUrl . search + redirectUrl . hash ,
84+ 'Cache-Control' : 'public, max-age=3600'
85+ }
86+ } ) ;
87+ }
88+ } catch ( error ) {
89+ // Log error but continue with original request
90+ console . error ( 'Trailing slash middleware error:' , error ) ;
91+ }
92+
93+ // No redirect found, continue with the original request
94+ return next ( ) ;
95+ }
0 commit comments