From d95be6bbee981f5ac0d152c9fb7e152bfa34f2fc Mon Sep 17 00:00:00 2001 From: "j.w.jonkers" Date: Sun, 10 May 2026 21:12:44 +0200 Subject: [PATCH] frontend: content-hash filenames, fix HTML/static cache headers, enable gzip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Vite output is now `assets/[hash].(js|ext)` (no source-derived `[name]` prefix). Filenames change only when bytes change, so unchanged chunks and assets keep their URLs across deploys and stay cached. - nginx serves index.html and unhashed root statics (favicon.png, banner.webp) with `Cache-Control: no-cache`, fixing two bugs: stale index.html pinning users to vanished chunk URLs after a deploy (NS_BINDING_ABORTED, "Unable to preload CSS …"), and unhashed images being served `immutable, max-age=31536000` so updates never reach cached clients. - `/assets/` location switched to `^~` so the prefix wins over regex location matching; the catch-all extension regex (which was masking the prefix and applying immutable to unhashed files) is removed. - nginx gzip on for js/css/json/svg/wasm/fonts; `gzip_static on` so any future precompressed artefacts are served automatically. Co-Authored-By: Claude Opus 4.7 (1M context) --- services/frontend/nginx.conf | 37 ++++++++++++++++++++++++------- services/frontend/vite.config.mjs | 9 +++++++- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/services/frontend/nginx.conf b/services/frontend/nginx.conf index 9186aa774..2b127fea3 100644 --- a/services/frontend/nginx.conf +++ b/services/frontend/nginx.conf @@ -4,16 +4,33 @@ server { root /usr/share/nginx/html; index index.html; - location /assets/ { - try_files $uri =404; - access_log off; - add_header Cache-Control "public, max-age=31536000, immutable"; - } + gzip on; + gzip_static on; + gzip_vary on; + gzip_min_length 256; + gzip_comp_level 6; + gzip_proxied any; + gzip_types + text/plain + text/css + text/javascript + application/javascript + application/json + application/xml + application/wasm + image/svg+xml + font/ttf + font/otf + font/woff + font/woff2; - location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|webp|txt|json|map)$ { + # Vite-built, content-hashed bundle output. Filename changes when bytes change, + # so we can hand out a one-year immutable cache. ^~ ensures this prefix wins + # over any regex location. + location ^~ /assets/ { try_files $uri =404; access_log off; - add_header Cache-Control "public, max-age=31536000, immutable"; + add_header Cache-Control "public, max-age=31536000, immutable" always; } location = /healthz { @@ -21,7 +38,11 @@ server { add_header Content-Type text/plain; } + # SPA entry document and unhashed root statics (favicon.png, banner.webp, ...). + # These have stable URLs but mutable content, so we force revalidation on every + # request. ETag/Last-Modified make 304s cheap. location / { + add_header Cache-Control "no-cache" always; try_files $uri /index.html; } -} \ No newline at end of file +} diff --git a/services/frontend/vite.config.mjs b/services/frontend/vite.config.mjs index a6ecaa66b..809e8f5f6 100644 --- a/services/frontend/vite.config.mjs +++ b/services/frontend/vite.config.mjs @@ -8,7 +8,14 @@ import istanbul from 'vite-plugin-istanbul' export default defineConfig({ build: { - target: "esnext" + target: "esnext", + rollupOptions: { + output: { + entryFileNames: 'assets/[hash].js', + chunkFileNames: 'assets/[hash].js', + assetFileNames: 'assets/[hash][extname]', + }, + }, }, css: { preprocessorOptions: {