Fix stale frontend cache handling

This commit is contained in:
Xenthys
2026-04-25 02:59:34 +02:00
parent 2171d13499
commit 6267e5d07b
5 changed files with 94 additions and 17 deletions
+17 -1
View File
@@ -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;
}
+17 -1
View File
@@ -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;
}
+3 -13
View File
@@ -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;
+29 -1
View File
@@ -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();
+28 -1
View File
@@ -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;