From aa02ab6a92d0d91e6860355ae5080af83caa2eb4 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:16:01 -0400 Subject: [PATCH 1/2] fix: serve static files with the Content-Type recorded in the manifest --- .../adapter-node-manifest-mime-types.md | 5 ++++ packages/adapter-node/src/handler.js | 26 ++++++++++-------- .../test/apps/basic/static/page.html | 2 ++ .../test/apps/basic/static/test.ico | Bin 0 -> 70 bytes .../adapter-node/test/apps/basic/test/test.js | 13 +++++++++ 5 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 .changeset/adapter-node-manifest-mime-types.md create mode 100644 packages/adapter-node/test/apps/basic/static/page.html create mode 100644 packages/adapter-node/test/apps/basic/static/test.ico diff --git a/.changeset/adapter-node-manifest-mime-types.md b/.changeset/adapter-node-manifest-mime-types.md new file mode 100644 index 000000000000..1d59ca72c56b --- /dev/null +++ b/.changeset/adapter-node-manifest-mime-types.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-node': patch +--- + +fix: serve static files with the Content-Type recorded in the manifest diff --git a/packages/adapter-node/src/handler.js b/packages/adapter-node/src/handler.js index 5c6739634263..a8ac21dd05d5 100644 --- a/packages/adapter-node/src/handler.js +++ b/packages/adapter-node/src/handler.js @@ -45,17 +45,21 @@ function serve(path, client = false) { etag: true, gzip: PRECOMPRESS, brotli: PRECOMPRESS, - setHeaders: client - ? (res, pathname) => { - // only apply to build directory, not e.g. version.json - if ( - pathname.startsWith(`/${manifest.appPath}/immutable/`) && - res.statusCode === 200 - ) { - res.setHeader('cache-control', 'public,max-age=31536000,immutable'); - } - } - : undefined + setHeaders: (res, pathname) => { + // `sirv` uses its own bundled `mrmime`, which the manifest's added types never reach + let type = manifest.mimeTypes[pathname.slice(pathname.lastIndexOf('.'))]; + if (type === 'text/html') type += ';charset=utf-8'; + if (type) res.setHeader('content-type', type); + + // only apply to build directory, not e.g. version.json + if ( + client && + pathname.startsWith(`/${manifest.appPath}/immutable/`) && + res.statusCode === 200 + ) { + res.setHeader('cache-control', 'public,max-age=31536000,immutable'); + } + } }) : undefined; } diff --git a/packages/adapter-node/test/apps/basic/static/page.html b/packages/adapter-node/test/apps/basic/static/page.html new file mode 100644 index 000000000000..3ec9ee860d0f --- /dev/null +++ b/packages/adapter-node/test/apps/basic/static/page.html @@ -0,0 +1,2 @@ + +

hi

diff --git a/packages/adapter-node/test/apps/basic/static/test.ico b/packages/adapter-node/test/apps/basic/static/test.ico new file mode 100644 index 0000000000000000000000000000000000000000..5b94bb90acc5340989e79df88d25b4942ab86f30 GIT binary patch literal 70 jcmZQzU<5%%1|X@xpuxbvAO^%B8YslT1f~^$Bz^z@B3}To literal 0 HcmV?d00001 diff --git a/packages/adapter-node/test/apps/basic/test/test.js b/packages/adapter-node/test/apps/basic/test/test.js index a3bae54d9c1f..ca7d7d03c0d6 100644 --- a/packages/adapter-node/test/apps/basic/test/test.js +++ b/packages/adapter-node/test/apps/basic/test/test.js @@ -22,3 +22,16 @@ test('does not set X-Accel-Buffering header on other responses', async ({ reques const response = await request.get('/'); expect(response.headers()['x-accel-buffering']).toBeUndefined(); }); + +test('serves static files with the Content-Type from the manifest', async ({ request }) => { + // https://github.com/sveltejs/kit/issues/13753 + const response = await request.get('/test.ico'); + expect(response.status()).toBe(200); + expect(response.headers()['content-type']).toBe('image/x-icon'); +}); + +test('serves static HTML with a charset', async ({ request }) => { + const response = await request.get('/page.html'); + expect(response.status()).toBe(200); + expect(response.headers()['content-type']).toBe('text/html;charset=utf-8'); +}); From 31f8060356a464827a50377fc5df2fd9a980728b Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:26:24 -0400 Subject: [PATCH 2/2] fix: record the mime types of prerendered paths in the server manifest --- .changeset/kit-prerendered-mime-types.md | 5 +++++ .../test/apps/basic/src/routes/prerendered.ico/+server.js | 7 +++++++ packages/adapter-node/test/apps/basic/test/test.js | 8 ++++++++ packages/kit/src/core/generate_manifest/index.js | 6 ++++++ 4 files changed, 26 insertions(+) create mode 100644 .changeset/kit-prerendered-mime-types.md create mode 100644 packages/adapter-node/test/apps/basic/src/routes/prerendered.ico/+server.js diff --git a/.changeset/kit-prerendered-mime-types.md b/.changeset/kit-prerendered-mime-types.md new file mode 100644 index 000000000000..c368d781dcb8 --- /dev/null +++ b/.changeset/kit-prerendered-mime-types.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: record the mime types of prerendered paths in the server manifest diff --git a/packages/adapter-node/test/apps/basic/src/routes/prerendered.ico/+server.js b/packages/adapter-node/test/apps/basic/src/routes/prerendered.ico/+server.js new file mode 100644 index 000000000000..ea5bb293b791 --- /dev/null +++ b/packages/adapter-node/test/apps/basic/src/routes/prerendered.ico/+server.js @@ -0,0 +1,7 @@ +export const prerender = true; + +export function GET() { + return new Response(new Uint8Array([0, 0, 1, 0]), { + headers: { 'content-type': 'image/x-icon' } + }); +} diff --git a/packages/adapter-node/test/apps/basic/test/test.js b/packages/adapter-node/test/apps/basic/test/test.js index ca7d7d03c0d6..5f09a35b8f9d 100644 --- a/packages/adapter-node/test/apps/basic/test/test.js +++ b/packages/adapter-node/test/apps/basic/test/test.js @@ -30,6 +30,14 @@ test('serves static files with the Content-Type from the manifest', async ({ req expect(response.headers()['content-type']).toBe('image/x-icon'); }); +test('serves prerendered endpoints with the Content-Type from the manifest', async ({ + request +}) => { + const response = await request.get('/prerendered.ico'); + expect(response.status()).toBe(200); + expect(response.headers()['content-type']).toBe('image/x-icon'); +}); + test('serves static HTML with a charset', async ({ request }) => { const response = await request.get('/page.html'); expect(response.status()).toBe(200); diff --git a/packages/kit/src/core/generate_manifest/index.js b/packages/kit/src/core/generate_manifest/index.js index b83aa8fef7e4..f524e1da70d2 100644 --- a/packages/kit/src/core/generate_manifest/index.js +++ b/packages/kit/src/core/generate_manifest/index.js @@ -94,6 +94,12 @@ export function generate_manifest({ mime_types[ext] ??= mime_lookup(ext) || ''; } + // record extensions that only exist in prerendered output, e.g. a prerendered favicon.ico + for (const pathname of prerendered) { + const ext = path.extname(pathname); + if (ext) mime_types[ext] ??= mime_lookup(ext) || ''; + } + // prettier-ignore // String representation of /** @template {import('@sveltejs/kit').SSRManifest} T */