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:
Binary file not shown.
|
After Width: | Height: | Size: 785 KiB |
+10
-6
@@ -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');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<meta name="theme-color" content="#12141a" />
|
||||
<title>CC-Manager</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
@@ -41,6 +41,13 @@ export default function Layout({ children }: { children: ReactNode }) {
|
||||
</header>
|
||||
|
||||
<main className="content">{children}</main>
|
||||
|
||||
<footer className="footer">
|
||||
<img src="/harle.png" alt="" className="footer-mascot" />
|
||||
<span>
|
||||
Created by <strong>Harlekin</strong> <span className="credits-sep">|</span> CC-Head
|
||||
</span>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ export default function Login() {
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="login-card">
|
||||
<img src="/harle.png" alt="" className="login-mascot" />
|
||||
<h1>CC-Manager</h1>
|
||||
<p className="login-sub">
|
||||
{challenge ? 'Bestätigungscode eingeben' : 'Bitte anmelden'}
|
||||
@@ -136,6 +137,10 @@ export default function Login() {
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="credits">
|
||||
Created by <strong>Harlekin</strong> <span className="credits-sep">|</span> CC-Head
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -833,3 +833,80 @@ textarea {
|
||||
.usage-hint {
|
||||
margin: -0.5rem 0 1.25rem;
|
||||
}
|
||||
|
||||
/* --- Maskottchen und Credits --- */
|
||||
|
||||
.login-page {
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.login-mascot {
|
||||
display: block;
|
||||
width: 190px;
|
||||
height: auto;
|
||||
margin: -3.25rem auto 0.25rem;
|
||||
/* Der Charakter steht frei, ein weicher Schein hebt ihn vom Karton ab. */
|
||||
filter: drop-shadow(0 6px 18px rgba(91, 140, 255, 0.28));
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.credits {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.credits strong {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.credits-sep {
|
||||
opacity: 0.4;
|
||||
margin: 0 0.15rem;
|
||||
}
|
||||
|
||||
.shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.shell .content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.6rem;
|
||||
padding: 1.1rem 1.5rem 1.6rem;
|
||||
border-top: 1px solid var(--border);
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.footer strong {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.footer-mascot {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
/* Kopfbereich ausschneiden: ganzfigurig ist die Figur hier zu klein. */
|
||||
object-fit: cover;
|
||||
object-position: 50% 16%;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-input);
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.login-mascot {
|
||||
width: 140px;
|
||||
margin-top: -2.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user