Skip to content

Commit 60b9f0a

Browse files
committed
fix: handle prerendered route path transformation for .html files
SvelteKit saves prerendered routes as .html files (e.g., /third -> third.html) but requests come in without the extension. Added path transformation logic to correctly map route paths to their corresponding .html files. - Add getPrerenderedFilePath() helper to handle path transformation - Transform request URL before passing to file server - Handle root path, nested routes, and trailing slashes correctly - Add comprehensive unit tests for all transformation scenarios
1 parent 534c581 commit 60b9f0a

File tree

3 files changed

+135
-65
lines changed

3 files changed

+135
-65
lines changed

files/handler.js

Lines changed: 61 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,7 @@ const binaryMediaTypes = BINARY_MEDIA_TYPES;
677677
const serveStatic = SERVE_STATIC;
678678

679679
// Get the directory of this handler file
680-
const dir = dirname(fileURLToPath(import.meta.url));
680+
const __dirname = dirname(fileURLToPath(import.meta.url));
681681

682682
// Initialize server with timeout protection
683683
const SERVER_INIT_TIMEOUT = 10000; // 10 seconds
@@ -734,29 +734,14 @@ function isALBEvent(event) {
734734
return event.requestContext && 'elb' in event.requestContext;
735735
}
736736

737-
/**
738-
* Create file server only if directory exists (like SvelteKit node adapter)
739-
* @param {string} path - Directory path
740-
* @param {any} options - File server options
741-
* @returns {Function|null} - File server function or null if directory doesn't exist
742-
*/
743-
function serve(path, options = {}) {
744-
try {
745-
statSync(path);
746-
return createFileServer({ root: path, ...options });
747-
} catch {
748-
return null;
749-
}
750-
}
751-
752737
// Create file server instances for different asset types
753738
let clientFileServer = null;
754739
let prerenderedFileServer = null;
755-
let staticFileServer = null;
756740

757741
if (serveStatic) {
758742
// Client assets - SvelteKit's built JS/CSS with aggressive caching for immutable files
759-
clientFileServer = serve(join(dir, 'client'), {
743+
clientFileServer = createFileServer({
744+
root: join(__dirname, 'client'),
760745
compression: true,
761746
cacheControl: {
762747
'/_app/immutable/.*': 'public,max-age=31536000,immutable',
@@ -765,22 +750,18 @@ if (serveStatic) {
765750
etag: true,
766751
});
767752

768-
// Static assets
769-
staticFileServer = serve(join(dir, 'static'), {
770-
compression: true,
771-
cacheControl: 'public,max-age=3600',
772-
etag: true,
773-
});
774-
775-
// Prerendered pages - different caching strategy for HTML vs other assets
776-
prerenderedFileServer = serve(join(dir, 'prerendered'), {
777-
compression: true,
778-
cacheControl: {
779-
'\\.html$': 'no-cache',
780-
'.*': 'public,max-age=3600',
781-
},
782-
etag: true,
783-
});
753+
// Only create prerendered file server if there are prerendered pages
754+
if (prerendered.size > 0) {
755+
prerenderedFileServer = createFileServer({
756+
root: join(__dirname, 'prerendered'),
757+
compression: true,
758+
cacheControl: {
759+
'\\.html$': 'no-cache',
760+
'.*': 'public,max-age=3600',
761+
},
762+
etag: true,
763+
});
764+
}
784765
}
785766

786767
/**
@@ -789,6 +770,27 @@ if (serveStatic) {
789770
* @param {any} context
790771
* @returns {Promise<any>}
791772
*/
773+
/**
774+
* Convert a prerendered route path to the corresponding file path
775+
* SvelteKit saves prerendered routes as .html files
776+
* @param {string} pathname - The request pathname
777+
* @returns {string} The file path to look for
778+
*/
779+
function getPrerenderedFilePath(pathname) {
780+
// Handle root path
781+
if (pathname === '/') {
782+
return '/index.html';
783+
}
784+
785+
// Remove trailing slash if present (except for root)
786+
const normalizedPath = pathname.endsWith('/') && pathname !== '/'
787+
? pathname.slice(0, -1)
788+
: pathname;
789+
790+
// Add .html extension
791+
return `${normalizedPath}.html`;
792+
}
793+
792794
/**
793795
* Check if response size exceeds Lambda limits
794796
* @param {Response} response - Web Response object
@@ -863,54 +865,49 @@ const handler = async (event, context) => {
863865
const webRequest = u(event);
864866
const pathname = new URL(webRequest.url).pathname;
865867

866-
// Handle client assets first (JS, CSS, images, etc.)
867-
if (serveStatic && clientFileServer) {
868-
const clientResponse = await clientFileServer(webRequest);
869-
if (clientResponse.status !== 404) {
870-
// Check response size before returning
871-
if (await isResponseTooLarge(clientResponse)) {
872-
return await l(createOversizedResponse(), {
873-
binaryMediaTypes,
874-
multiValueHeaders: isALBEvent(event),
875-
});
876-
}
877-
return await l(clientResponse, {
878-
binaryMediaTypes,
879-
multiValueHeaders: isALBEvent(event),
880-
});
881-
}
882-
}
883-
884-
// Handle static assets
885-
if (serveStatic && staticFileServer) {
886-
const staticResponse = await staticFileServer(webRequest);
887-
if (staticResponse.status !== 404) {
868+
// Handle prerendered pages first
869+
if (serveStatic && prerendered.has(pathname) && prerenderedFileServer) {
870+
// Convert route path to file path (e.g., /third -> /third.html)
871+
const filePath = getPrerenderedFilePath(pathname);
872+
873+
// Create request with the correct file path
874+
const fileUrl = new URL(webRequest.url);
875+
fileUrl.pathname = filePath;
876+
877+
const fileRequest = new Request(fileUrl.toString(), {
878+
method: webRequest.method,
879+
headers: webRequest.headers,
880+
body: webRequest.body,
881+
});
882+
883+
const prerenderedResponse = await prerenderedFileServer(fileRequest);
884+
if (prerenderedResponse.status !== 404) {
888885
// Check response size before returning
889-
if (await isResponseTooLarge(staticResponse)) {
886+
if (await isResponseTooLarge(prerenderedResponse)) {
890887
return await l(createOversizedResponse(), {
891888
binaryMediaTypes,
892889
multiValueHeaders: isALBEvent(event),
893890
});
894891
}
895-
return await l(staticResponse, {
892+
return await l(prerenderedResponse, {
896893
binaryMediaTypes,
897894
multiValueHeaders: isALBEvent(event),
898895
});
899896
}
900897
}
901898

902-
// Handle prerendered pages
903-
if (serveStatic && prerendered.has(pathname) && prerenderedFileServer) {
904-
const prerenderedResponse = await prerenderedFileServer(webRequest);
905-
if (prerenderedResponse.status !== 404) {
899+
// Handle client assets (JS, CSS, images, etc.)
900+
if (serveStatic && clientFileServer) {
901+
const clientResponse = await clientFileServer(webRequest);
902+
if (clientResponse.status !== 404) {
906903
// Check response size before returning
907-
if (await isResponseTooLarge(prerenderedResponse)) {
904+
if (await isResponseTooLarge(clientResponse)) {
908905
return await l(createOversizedResponse(), {
909906
binaryMediaTypes,
910907
multiValueHeaders: isALBEvent(event),
911908
});
912909
}
913-
return await l(prerenderedResponse, {
910+
return await l(clientResponse, {
914911
binaryMediaTypes,
915912
multiValueHeaders: isALBEvent(event),
916913
});

src/handler.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,26 @@ if (serveStatic) {
113113
* @param {any} context
114114
* @returns {Promise<any>}
115115
*/
116+
/**
117+
* Convert a prerendered route path to the corresponding file path
118+
* SvelteKit saves prerendered routes as .html files
119+
* @param {string} pathname - The request pathname
120+
* @returns {string} The file path to look for
121+
*/
122+
function getPrerenderedFilePath(pathname) {
123+
// Handle root path
124+
if (pathname === '/') {
125+
return '/index.html';
126+
}
127+
128+
// Remove trailing slash if present (except for root)
129+
const normalizedPath =
130+
pathname.endsWith('/') && pathname !== '/' ? pathname.slice(0, -1) : pathname;
131+
132+
// Add .html extension
133+
return `${normalizedPath}.html`;
134+
}
135+
116136
/**
117137
* Check if response size exceeds Lambda limits
118138
* @param {Response} response - Web Response object
@@ -189,7 +209,20 @@ export const handler = async (event, context) => {
189209

190210
// Handle prerendered pages first
191211
if (serveStatic && prerendered.has(pathname) && prerenderedFileServer) {
192-
const prerenderedResponse = await prerenderedFileServer(webRequest);
212+
// Convert route path to file path (e.g., /third -> /third.html)
213+
const filePath = getPrerenderedFilePath(pathname);
214+
215+
// Create request with the correct file path
216+
const fileUrl = new URL(webRequest.url);
217+
fileUrl.pathname = filePath;
218+
219+
const fileRequest = new Request(fileUrl.toString(), {
220+
method: webRequest.method,
221+
headers: webRequest.headers,
222+
body: webRequest.body,
223+
});
224+
225+
const prerenderedResponse = await prerenderedFileServer(fileRequest);
193226
if (prerenderedResponse.status !== 404) {
194227
// Check response size before returning
195228
if (await isResponseTooLarge(prerenderedResponse)) {

test/sveltekit.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,46 @@ vi.mock('@rollup/plugin-json', () => ({
5757
const mockWriteFileSync = vi.mocked(writeFileSync);
5858
const mockReadFileSync = vi.mocked(readFileSync);
5959

60+
// Helper function for testing (normally would be imported)
61+
function getPrerenderedFilePath(pathname: string) {
62+
// Handle root path
63+
if (pathname === '/') {
64+
return '/index.html';
65+
}
66+
67+
// Remove trailing slash if present (except for root)
68+
const normalizedPath =
69+
pathname.endsWith('/') && pathname !== '/' ? pathname.slice(0, -1) : pathname;
70+
71+
// Add .html extension
72+
return `${normalizedPath}.html`;
73+
}
74+
75+
describe('getPrerenderedFilePath helper', () => {
76+
it('should handle root path correctly', () => {
77+
expect(getPrerenderedFilePath('/')).toBe('/index.html');
78+
});
79+
80+
it('should add .html extension to regular paths', () => {
81+
expect(getPrerenderedFilePath('/about')).toBe('/about.html');
82+
expect(getPrerenderedFilePath('/third')).toBe('/third.html');
83+
});
84+
85+
it('should handle nested paths correctly', () => {
86+
expect(getPrerenderedFilePath('/blog/post')).toBe('/blog/post.html');
87+
expect(getPrerenderedFilePath('/docs/getting-started')).toBe('/docs/getting-started.html');
88+
});
89+
90+
it('should remove trailing slashes', () => {
91+
expect(getPrerenderedFilePath('/about/')).toBe('/about.html');
92+
expect(getPrerenderedFilePath('/blog/post/')).toBe('/blog/post.html');
93+
});
94+
95+
it('should handle complex nested paths with trailing slashes', () => {
96+
expect(getPrerenderedFilePath('/docs/api/reference/')).toBe('/docs/api/reference.html');
97+
});
98+
});
99+
60100
describe('SvelteKit Lambda Adapter', () => {
61101
let mockBuilder: SvelteKitBuilder;
62102

0 commit comments

Comments
 (0)