perf: LCP-Fix, Banner-Crops, SSR-Cache, Bulk-Windkarte
Deploy / verify (push) Successful in 1m3s
Deploy / deploy (push) Successful in 1m47s

- TopBanner: JS-Fade-Gate entfernt (opacity-0 bis Hydration schob LCP
  von ~3s auf ~8.6s) — Bild malt jetzt progressiv über den LQIP
- buildBannerResponsive: Breakpoint-Varianten (1/1, 16/9, 24/9) mit
  media-Attributen, quality 60; CMS braucht explizites h neben ar,
  sonst kein Crop (Mobile bekam 1.92:1 unscharf hochskaliert)
- Layout-Preload: ein Link pro Breakpoint-Variante (WebP)
- Layout-Bootstrap-Batch + Logo-SVG-Fetch TTL-gecacht (liefen pro
  SSR-Request); invalidateCollection versteht CSV-Key-Heads
- WindkarteBlock: 40 Einzel-Fetches -> 1 List-Request mit Filter
- app.html: preconnect cms.pm86.de, api.iconify.design, analytics
- u-url Microformat: echte canonicalUrl statt href="#" (SEO-Audit)
- Icon-Subset: 'cloud' ergänzt (kein Runtime-Fetch an iconify mehr)
- StrommixBlock: nutzloses transition-colors entfernt (CLS-Audit)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-06-12 09:50:01 +02:00
parent 41597f87b5
commit d9519a867e
13 changed files with 164 additions and 89 deletions
+29 -12
View File
@@ -72,7 +72,7 @@
headline?: string;
subheadline?: string;
responsive?: {
sources: { type: string; srcset: string }[];
sources: { type: string; srcset: string; media?: string }[];
fallback: string;
width: number;
height: number;
@@ -118,13 +118,29 @@
});
const topBannerData = $derived(pageData?.topBanner ?? null);
const bannerPreload = $derived.by(() => {
// Ein Preload pro Breakpoint-Variante (media-Attribut), Format: WebP.
// AVIF wäre kleiner, aber ein type-Mismatch (Browser ohne AVIF) macht den
// Preload wirkungslos — WebP greift überall. Browser laden nur den Preload,
// dessen media-Query matcht.
const bannerPreloads = $derived.by(() => {
const r = topBannerData?.responsive;
if (!r) return null;
const webp = r.sources.find((s) => s.type === 'image/webp');
const picked = webp ?? r.sources[0];
if (!picked) return { href: r.fallback, srcset: undefined as string | undefined, type: undefined as string | undefined };
return { href: r.fallback, srcset: picked.srcset, type: picked.type };
if (!r) return [];
type Src = { type: string; srcset: string; media?: string };
const sources = r.sources as Src[];
const byMedia = new Map<string, Src>();
for (const s of sources) {
const key = s.media ?? '';
const existing = byMedia.get(key);
if (!existing || (existing.type !== 'image/webp' && s.type === 'image/webp')) {
if (s.type === 'image/webp' || !existing) byMedia.set(key, s);
}
}
return [...byMedia.values()].map((s) => ({
href: r.fallback,
srcset: s.srcset,
type: s.type,
media: s.media,
}));
});
function escapeLdJson(s: string): string {
@@ -204,17 +220,18 @@
{:else}
<meta name="theme-color" content="#ffffff" />
<meta name="robots" content={robotsContent} />
{#if bannerPreload}
{#each bannerPreloads as p (p.media ?? p.type)}
<link
rel="preload"
as="image"
href={bannerPreload.href}
imagesrcset={bannerPreload.srcset}
href={p.href}
imagesrcset={p.srcset}
imagesizes="100vw"
type={bannerPreload.type}
type={p.type}
media={p.media}
fetchpriority="high"
/>
{/if}
{/each}
<link rel="icon" type={faviconType} href={faviconUrl} />
<link rel="apple-touch-icon" href={faviconUrl} />
<link rel="alternate" type="application/rss+xml" title="Beiträge" href="/posts/rss.xml" />