perf: LCP-Fix, Banner-Crops, SSR-Cache, Bulk-Windkarte
- 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:
+29
-12
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user