@@ -9,15 +9,15 @@ const path = require('node:path')
99const source = fs . readFileSync ( path . join ( __dirname , 'redirects.js' ) , 'utf8' )
1010const handler = new Function ( source + '\nreturn handler;' ) ( )
1111
12- function buildEvent ( uri , host ) {
12+ function buildEvent ( uri , host , querystring ) {
1313 return {
1414 version : '1.0' ,
1515 context : { } ,
1616 viewer : { } ,
1717 request : {
1818 method : 'GET' ,
1919 uri : uri ,
20- querystring : { } ,
20+ querystring : querystring || { } ,
2121 headers : host ? { host : { value : host } } : { } ,
2222 cookies : { } ,
2323 } ,
@@ -57,7 +57,7 @@ test('/docsfoo → NOT redirected (not under /docs/)', () => {
5757 assert . equal ( result . uri , '/index.html' )
5858} )
5959
60- test ( '/llms.txt → pass through unchanged (matches current 404 behavior )' , ( ) => {
60+ test ( '/llms.txt → pass through unchanged (static file )' , ( ) => {
6161 const result = handler ( buildEvent ( '/llms.txt' , 'iii.dev' ) )
6262 assert . ok ( ! isRedirect ( result ) )
6363 assert . equal ( result . uri , '/llms.txt' )
@@ -81,6 +81,58 @@ test('www.iii.dev/docs/foo → 301 https://iii.dev/docs/foo', () => {
8181 assert . equal ( locationOf ( result ) , 'https://iii.dev/docs/foo' )
8282} )
8383
84+ test ( 'www.iii.dev preserves querystring with multiValue and empty params' , ( ) => {
85+ // Mirrors the CloudFront Functions querystring shape: repeated keys spill into
86+ // multiValue, value-less keys arrive as empty strings, and special chars must
87+ // be re-encoded.
88+ const result = handler (
89+ buildEvent ( '/some/page' , 'www.iii.dev' , {
90+ a : { value : '1' , multiValue : [ { value : '2' } ] } ,
91+ empty : { value : '' } ,
92+ ref : { value : 'hello world' } ,
93+ } ) ,
94+ )
95+ assert . ok ( isRedirect ( result ) )
96+ assert . equal (
97+ locationOf ( result ) ,
98+ 'https://iii.dev/some/page?a=1&a=2&empty=&ref=hello%20world' ,
99+ )
100+ } )
101+
102+ test ( 'www.iii.dev with no querystring → no trailing ?' , ( ) => {
103+ const result = handler ( buildEvent ( '/some/page' , 'www.iii.dev' , { } ) )
104+ assert . ok ( isRedirect ( result ) )
105+ assert . equal ( locationOf ( result ) , 'https://iii.dev/some/page' )
106+ } )
107+
108+ test ( 'www.iii.dev percent-encodes reserved chars in keys and values' , ( ) => {
109+ // Values containing &, =, #, + would otherwise corrupt the redirect target
110+ // (& splits params, # ends the URL into a fragment, + flips to space on parse,
111+ // = confuses some clients). Keys with spaces must also be encoded.
112+ const result = handler (
113+ buildEvent ( '/p' , 'www.iii.dev' , {
114+ 'weird key' : { value : 'a&b=c+d#e' } ,
115+ } ) ,
116+ )
117+ assert . ok ( isRedirect ( result ) )
118+ assert . equal (
119+ locationOf ( result ) ,
120+ 'https://iii.dev/p?weird%20key=a%26b%3Dc%2Bd%23e' ,
121+ )
122+ } )
123+
124+ test ( 'SPA fallback preserves querystring on the request object (no rewrite)' , ( ) => {
125+ // The handler mutates request.uri but returns the same request object, so
126+ // CloudFront forwards the original querystring untouched. Pin the no-op so
127+ // a future refactor doesn't accidentally clear it.
128+ const qs = { utm_source : { value : 'twitter' } , ref : { value : 'launch' } }
129+ const event = buildEvent ( '/some/route' , 'iii.dev' , qs )
130+ const result = handler ( event )
131+ assert . ok ( ! isRedirect ( result ) )
132+ assert . equal ( result . uri , '/index.html' )
133+ assert . equal ( result . querystring , qs )
134+ } )
135+
84136test ( '/ (root) → pass through unchanged' , ( ) => {
85137 const result = handler ( buildEvent ( '/' , 'iii.dev' ) )
86138 assert . ok ( ! isRedirect ( result ) )
@@ -93,10 +145,16 @@ test('/some/client/route → rewrite uri to /index.html', () => {
93145 assert . equal ( result . uri , '/index.html' )
94146} )
95147
96- test ( '/manifesto → rewrite uri to /index .html' , ( ) => {
148+ test ( '/manifesto → rewrite uri to /manifesto .html (flat HTML, Option A) ' , ( ) => {
97149 const result = handler ( buildEvent ( '/manifesto' , 'iii.dev' ) )
98150 assert . ok ( ! isRedirect ( result ) )
99- assert . equal ( result . uri , '/index.html' )
151+ assert . equal ( result . uri , '/manifesto.html' )
152+ } )
153+
154+ test ( '/AGENTS.md → pass through unchanged' , ( ) => {
155+ const result = handler ( buildEvent ( '/AGENTS.md' , 'iii.dev' ) )
156+ assert . ok ( ! isRedirect ( result ) )
157+ assert . equal ( result . uri , '/AGENTS.md' )
100158} )
101159
102160test ( '/foo/ trailing slash → pass through unchanged (no SPA rewrite)' , ( ) => {
@@ -111,10 +169,10 @@ test('/missing.jpg → pass through unchanged (S3 returns 404)', () => {
111169 assert . equal ( result . uri , '/missing.jpg' )
112170} )
113171
114- test ( '/ai/index.html → pass through unchanged ' , ( ) => {
115- const result = handler ( buildEvent ( '/ai/index.html ' , 'iii.dev' ) )
172+ test ( '/ai → SPA fallback to /index.html ' , ( ) => {
173+ const result = handler ( buildEvent ( '/ai' , 'iii.dev' ) )
116174 assert . ok ( ! isRedirect ( result ) )
117- assert . equal ( result . uri , '/ai/ index.html' )
175+ assert . equal ( result . uri , '/index.html' )
118176} )
119177
120178test ( '/assets/main.abc123.js → pass through unchanged' , ( ) => {
0 commit comments