CSS/JS mit Versionsnummer ausliefern, damit nach Updates kein alter Cache greift

Der Nginx Proxy Manager ("Cache Assets") und Browser hielten das alte
common.js vor, die neue Startseite brach mit fehlendem Export ab.
HTML und JS werden jetzt mit no-cache ausgeliefert, CSS/JS-Adressen und
Modul-Importe bekommen ?v=<Prüfsumme> angehängt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
guyg1474andClaude Opus 5.5 committed 2026-10-12 00:25:24 +02:00
1 parent 486e6f6784
commit f46016487d
1 file changed
+45 -3
+45 -3
View File
@@ -1,7 +1,7 @@
import express from 'express'; import express from 'express';
import { randomUUID } from 'node:crypto'; import { randomUUID, createHash } from 'node:crypto';
import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync, unlinkSync } from 'node:fs'; import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync, unlinkSync, readdirSync, statSync } from 'node:fs';
import { dirname, join } from 'node:path'; import { dirname, join, extname, normalize, sep } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { authRoutes, requireAuth } from './auth.js'; import { authRoutes, requireAuth } from './auth.js';
@@ -178,6 +178,46 @@ function findOr404(list, id, res, what) {
return item; return item;
} }
// ---------- Versionierte Seiten ----------
// Proxys (z. B. Nginx Proxy Manager mit "Cache Assets") und Browser halten CSS/JS lange vor. Damit nach einem
// Update nie alte Dateien mit neuen gemischt werden, hängt der Server an jede CSS/JS-Adresse in HTML und an
// jeden Modul-Import ?v=<Prüfsumme aller CSS/JS-Dateien> an. HTML und JS selbst werden immer neu geprüft.
const ASSET_DIRS = ['public/css', 'public/js', 'private/js'].map((d) => join(ROOT, d));
let assetVersion = { key: '', value: '' };
function currentAssetVersion() {
const files = ASSET_DIRS.flatMap((dir) => (existsSync(dir) ? readdirSync(dir).map((f) => join(dir, f)) : []))
.filter((f) => /\.(css|js)$/.test(f)).sort();
// Nur neu rechnen, wenn sich Dateien geändert haben (Änderungszeit + Größe)
const key = files.map((f) => { const st = statSync(f); return `${f}:${st.mtimeMs}:${st.size}`; }).join('|');
if (key !== assetVersion.key) {
const hash = createHash('sha1');
for (const f of files) hash.update(readFileSync(f));
assetVersion = { key, value: hash.digest('hex').slice(0, 10) };
}
return assetVersion.value;
}
function versioned(dir, cacheControl) {
return (req, res, next) => {
if (req.method !== 'GET' && req.method !== 'HEAD') return next();
let path;
try { path = decodeURIComponent(req.path); } catch { return next(); }
if (path.endsWith('/')) path += 'index.html';
let file = normalize(join(dir, path));
if (!file.startsWith(dir + sep)) return next();
if (!extname(file)) file += '.html';
if (!/\.(html|js)$/.test(file) || !existsSync(file) || !statSync(file).isFile()) return next();
const v = currentAssetVersion();
const text = readFileSync(file, 'utf8')
.replace(/((?:href|src)="\/(?:css|js)\/[\w.-]+\.(?:css|js))"/g, `$1?v=${v}"`)
.replace(/(from\s+'\.\/[\w.-]+\.js)'/g, `$1?v=${v}'`);
res.type(extname(file));
res.setHeader('Cache-Control', cacheControl);
res.send(text);
};
}
// ---------- App ---------- // ---------- App ----------
const app = express(); const app = express();
app.disable('x-powered-by'); app.disable('x-powered-by');
@@ -189,6 +229,7 @@ app.use('/api/patches', express.json({ limit: '12mb' }));
app.use(express.json({ limit: '1mb' })); app.use(express.json({ limit: '1mb' }));
// Öffentlich: Startseite, Login, CSS/Bilder // Öffentlich: Startseite, Login, CSS/Bilder
app.use(versioned(join(ROOT, 'public'), 'no-cache'));
app.use(express.static(join(ROOT, 'public'), { extensions: ['html'] })); app.use(express.static(join(ROOT, 'public'), { extensions: ['html'] }));
authRoutes(app); authRoutes(app);
@@ -309,6 +350,7 @@ app.delete('/api/patches/:id', (req, res) => {
saveDb(); saveDb();
res.status(204).end(); res.status(204).end();
}); });
app.use(versioned(join(ROOT, 'private'), 'private, no-cache'));
app.use(express.static(join(ROOT, 'private'), { app.use(express.static(join(ROOT, 'private'), {
extensions: ['html'], extensions: ['html'],
setHeaders: (res) => res.setHeader('Cache-Control', 'private, no-cache'), setHeaders: (res) => res.setHeader('Cache-Control', 'private, no-cache'),