diff --git a/apps/server/src/http.test.ts b/apps/server/src/http.test.ts index ec4d2aae16e..f85de08d40b 100644 --- a/apps/server/src/http.test.ts +++ b/apps/server/src/http.test.ts @@ -44,4 +44,15 @@ describe("assetResponseHeaders", () => { "X-Content-Type-Options": "nosniff", }); }); + + it("declares utf-8 for HTML assets so non-ASCII content renders correctly", () => { + expect(assetResponseHeaders("/workspace/page.html")).toHaveProperty( + "Content-Type", + "text/html; charset=utf-8", + ); + expect(assetResponseHeaders("/workspace/PAGE.HTM")).toHaveProperty( + "Content-Type", + "text/html; charset=utf-8", + ); + }); }); diff --git a/apps/server/src/http.ts b/apps/server/src/http.ts index 0da55686b92..be133399e0f 100644 --- a/apps/server/src/http.ts +++ b/apps/server/src/http.ts @@ -46,10 +46,14 @@ const DESKTOP_RENDERER_ORIGINS = ["t3code://app", "t3code-dev://app"]; const SVG_CONTENT_SECURITY_POLICY = "default-src 'none'; style-src 'unsafe-inline'; sandbox"; export function assetResponseHeaders(filePath: string): Record { + const lowerPath = filePath.toLowerCase(); return { "Cache-Control": "private, max-age=3600", "X-Content-Type-Options": "nosniff", - ...(filePath.toLowerCase().endsWith(".svg") + ...(lowerPath.endsWith(".html") || lowerPath.endsWith(".htm") + ? { "Content-Type": "text/html; charset=utf-8" } + : {}), + ...(lowerPath.endsWith(".svg") ? { "Content-Security-Policy": SVG_CONTENT_SECURITY_POLICY } : {}), };