Maskottchen als Symbol und Credits

harle.png steht jetzt gross auf der Anmeldeseite und klein in der Fusszeile,
dazu die Zeile "Created by Harlekin | CC-Head".

Aus der Vorlage entstehen drei Groessen: das volle Bild fuer die Anzeige und
ein Ausschnitt des Kopfes fuer Favicon und Startsymbol - ganzfigurig waere die
Figur bei 32 Pixeln nicht mehr zu erkennen.

Dabei fiel auf, dass die Cache-Regel zu grob war: sie gab allem ausser HTML
ein Jahr immutable, also auch Bildern. Deren Name aendert sich beim Austausch
aber nicht, ein neues Bild waere ein Jahr lang nicht angekommen. Jetzt gilt
das lange Ablaufdatum nur fuer die gebauten Assets, die einen Inhalts-Hash im
Namen tragen; alles andere haelt einen Tag und fragt dann nach.
This commit is contained in:
2026-09-18 19:44:29 +02:00
parent bebb89c10e
commit 3344592c6e
10 changed files with 112 additions and 6 deletions
+10 -6
View File
@@ -1,5 +1,5 @@
import { existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { dirname, join, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import Fastify, { type FastifyInstance } from 'fastify';
import cookie from '@fastify/cookie';
@@ -135,13 +135,17 @@ export async function buildApp(): Promise<FastifyInstance> {
// sonst zeigt der Browser nach einem Update monatelang die alte Oberfläche.
// Deshalb hier abschalten und beides unten selbst setzen.
cacheControl: false,
setHeaders: (res, path) => {
if (path.endsWith('.html')) {
setHeaders: (res, filePath) => {
if (filePath.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache, must-revalidate');
} else {
// Assets tragen einen Inhalts-Hash im Namen: ändert sich der Inhalt,
// ändert sich der Dateiname. Die dürfen beliebig lange liegen.
} else if (filePath.includes(`${sep}assets${sep}`)) {
// Nur die gebauten Assets tragen einen Inhalts-Hash im Namen: ändert
// sich der Inhalt, ändert sich der Dateiname. Die dürfen liegen bleiben.
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
} else {
// Alles andere - Bilder, Favicon - behält den Namen bei neuem Inhalt.
// Ein Tag mit Rückfrage, damit ein Austausch auch ankommt.
res.setHeader('Cache-Control', 'public, max-age=86400, must-revalidate');
}
},
});
+10
View File
@@ -309,6 +309,16 @@ describe('Auslieferung der Oberfläche', () => {
assert.match(response.headers['cache-control'] as string, /no-cache/);
});
it('lässt Bilder nicht ein Jahr lang kleben', async () => {
// Sie tragen keinen Hash im Namen - ein Austausch muss ankommen.
const response = await app.inject({ method: 'GET', url: '/favicon.png' });
assert.equal(response.statusCode, 200);
const header = response.headers['cache-control'] as string;
assert.doesNotMatch(header, /immutable/, 'kein immutable ohne Hash im Namen');
assert.match(header, /must-revalidate/);
});
it('lässt gehashte Assets lange liegen', async () => {
const page = await app.inject({ method: 'GET', url: '/' });
const asset = /\/assets\/[^"]+\.js/.exec(page.body)?.[0];