diff --git a/docker/nginx-https.conf b/docker/nginx-https.conf index 719828fa..35294599 100644 --- a/docker/nginx-https.conf +++ b/docker/nginx-https.conf @@ -65,16 +65,32 @@ http { add_header X-Content-Type-Options nosniff always; add_header X-XSS-Protection "1; mode=block" always; + location = /sw.js { + root /app/html; + expires off; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; + try_files $uri =404; + } + + location = /manifest.json { + root /app/html; + expires off; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; + try_files $uri =404; + } + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { root /app/html; expires 1y; - add_header Cache-Control "public, immutable"; + add_header Cache-Control "public, max-age=31536000, immutable" always; try_files $uri =404; } location / { root /app/html; index index.html index.htm; + expires off; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; try_files $uri $uri/ /index.html; } diff --git a/docker/nginx.conf b/docker/nginx.conf index 2781e3ec..ffa01bc2 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -54,16 +54,32 @@ http { add_header X-Content-Type-Options nosniff always; add_header X-XSS-Protection "1; mode=block" always; + location = /sw.js { + root /app/html; + expires off; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; + try_files $uri =404; + } + + location = /manifest.json { + root /app/html; + expires off; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; + try_files $uri =404; + } + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { root /app/html; expires 1y; - add_header Cache-Control "public, immutable"; + add_header Cache-Control "public, max-age=31536000, immutable" always; try_files $uri =404; } location / { root /app/html; index index.html index.htm; + expires off; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; try_files $uri $uri/ /index.html; } diff --git a/public/sw.js b/public/sw.js index a15adc8d..a3fbfcb6 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,8 +1,5 @@ -const CACHE_NAME = "termix-v1"; +const CACHE_NAME = "termix-static-v2"; const STATIC_ASSETS = [ - "/", - "/index.html", - "/manifest.json", "/favicon.ico", "/icons/48x48.png", "/icons/128x128.png", @@ -66,18 +63,11 @@ self.addEventListener("fetch", (event) => { } if (request.mode === "navigate") { - event.respondWith( - fetch(request).catch(() => { - return caches.match("/index.html"); - }), - ); + event.respondWith(fetch(request)); return; } - const isStaticAsset = STATIC_ASSETS.some((asset) => { - if (asset === "/") return url.pathname === "/"; - return url.pathname === asset || url.pathname.startsWith("/assets/"); - }); + const isStaticAsset = STATIC_ASSETS.some((asset) => url.pathname === asset); if (!isStaticAsset) { return; diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index 35bf93f1..09319603 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -1771,10 +1771,38 @@ if (frontendDist) { databaseLogger.info(`Serving frontend from: ${frontendDist}`, { operation: "static_files", }); - app.use(express.static(frontendDist)); + app.use( + express.static(frontendDist, { + setHeaders: (res, filePath) => { + const relativePath = path + .relative(frontendDist, filePath) + .replaceAll(path.sep, "/"); + + if (relativePath.startsWith("assets/")) { + res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); + return; + } + + if ( + relativePath === "index.html" || + relativePath === "sw.js" || + relativePath === "manifest.json" + ) { + res.setHeader( + "Cache-Control", + "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0", + ); + } + }, + }), + ); app.use((req, res, next) => { if (req.method === "GET" && req.accepts("html")) { + res.setHeader( + "Cache-Control", + "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0", + ); res.sendFile(path.join(frontendDist, "index.html")); } else { next(); diff --git a/src/hooks/use-service-worker.ts b/src/hooks/use-service-worker.ts index f7d12781..6e38c455 100644 --- a/src/hooks/use-service-worker.ts +++ b/src/hooks/use-service-worker.ts @@ -40,27 +40,54 @@ export function useServiceWorker(): ServiceWorkerState { if (!isSupported) return; + const shouldReloadOnControllerChange = Boolean( + navigator.serviceWorker.controller, + ); + let hasReloadedForUpdate = false; + const handleControllerChange = () => { + if (!shouldReloadOnControllerChange || hasReloadedForUpdate) { + return; + } + + hasReloadedForUpdate = true; + window.location.reload(); + }; + const registerSW = async () => { try { const registration = await navigator.serviceWorker.register( `${getBasePath()}/sw.js`, + { updateViaCache: "none" }, ); setState((prev) => ({ ...prev, isRegistered: true })); registration.addEventListener("updatefound", () => handleUpdateFound(registration), ); + await registration.update(); } catch (error) { console.error("[SW] Registration failed:", error); } }; + navigator.serviceWorker.addEventListener( + "controllerchange", + handleControllerChange, + ); + if (document.readyState === "complete") { registerSW(); } else { window.addEventListener("load", registerSW); - return () => window.removeEventListener("load", registerSW); } + + return () => { + window.removeEventListener("load", registerSW); + navigator.serviceWorker.removeEventListener( + "controllerchange", + handleControllerChange, + ); + }; }, [handleUpdateFound]); return state;