feat: update site URLs and enhance image handling
Deploy / verify (push) Successful in 50s
Deploy / deploy (push) Failing after 1m3s

- Changed site URLs in docker-compose and deployment configurations to remove "www" prefix for consistency.
- Improved image handling by introducing warmPostCardImages function for pre-fetching images in posts and tags.
- Enhanced PostCard component with responsive image sizes for better performance.
- Updated layout to include robots meta tag and JSON-LD structured data for improved SEO.
- Added loading state for images in TopBanner component to enhance user experience.
This commit is contained in:
Peter Meier
2026-04-18 12:25:45 +02:00
parent a2f2d5bfb0
commit da294010c8
16 changed files with 322 additions and 29 deletions
+1
View File
@@ -47,6 +47,7 @@
width={400}
aspect="3/2"
alt={post.headline ?? ""}
sizes="(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw"
class="w-full h-full object-cover"
loading="lazy"
/>
+22 -2
View File
@@ -80,6 +80,19 @@
const hasPageTitle = $derived((headline != null && headline !== "") || (subheadline != null && subheadline !== ""));
const showBanner = $derived(hasImage || hasText || hasPageTitle);
const altText = $derived(banner?.headline ?? headline ?? "");
let imgEl = $state<HTMLImageElement | null>(null);
let imgLoaded = $state(false);
const currentSrc = $derived(responsive?.fallback ?? resolvedImages[0] ?? "");
$effect(() => {
// reset on src change, then probe cache
void currentSrc;
imgLoaded = false;
queueMicrotask(() => {
if (imgEl?.complete && imgEl.naturalWidth > 0) imgLoaded = true;
});
});
</script>
{#if showBanner}
@@ -98,13 +111,17 @@
<img
src={responsive.fallback}
alt={altText}
class="w-full object-cover max-h-[400px] lg:max-h-[600px] max-w-[1920px] aspect-square sm:aspect-video lg:aspect-24/9 text-transparent"
class="w-full object-cover max-h-[400px] lg:max-h-[600px] max-w-[1920px] aspect-square sm:aspect-video lg:aspect-24/9 text-transparent transition-opacity duration-500"
class:opacity-0={!imgLoaded}
class:opacity-100={imgLoaded}
width={responsive.width}
height={responsive.height}
style={imgStyle}
loading="eager"
fetchpriority="high"
decoding="async"
bind:this={imgEl}
onload={() => (imgLoaded = true)}
/>
</picture>
{/key}
@@ -113,12 +130,15 @@
<img
src={resolvedImages[0]}
alt={altText}
class="w-full object-cover max-h-[400px] lg:max-h-[600px] max-w-[1920px] aspect-square sm:aspect-video lg:aspect-24/9 text-transparent"
class="w-full object-cover max-h-[400px] lg:max-h-[600px] max-w-[1920px] aspect-square sm:aspect-video lg:aspect-24/9 text-transparent transition-opacity duration-500"
class:opacity-0={!imgLoaded}
class:opacity-100={imgLoaded}
width={1920}
height={600}
style={imgStyle}
loading="eager"
fetchpriority="high"
onload={() => (imgLoaded = true)}
/>
{/key}
{/if}
+15 -3
View File
@@ -7,10 +7,22 @@
export const DEFAULT_SOCIAL_IMAGE_URL =
"https://images.ctfassets.net/xjxq6v7l1pfe/43yZHpF9OCnrBlEWHJSZMK/1ef20b265ea1e4e55317812ee5ae6b4c/simple-green-tree-illustrated-watercolor-with-gentle-shading-great-rural-scenery-farm-backdrops-naturethemed-designs-landsca.jpg";
/** Parameter für og:image / twitter:image (über /cms-images + Disk-Cache). */
/** og:image-Dimensionen (Facebook/Twitter empfehlen 1200×630, 1.91:1). */
export const SOCIAL_IMAGE_DIMENSIONS = {
width: 1200,
height: 630,
} as const;
/** Logo-Fallback für og:image: letterboxed (contain), damit Logo komplett sichtbar bleibt. */
export const SOCIAL_IMAGE_TRANSFORM = {
width: 200,
height: 200,
...SOCIAL_IMAGE_DIMENSIONS,
fit: "contain" as const,
format: "webp" as const,
};
/** Post-Bild für og:image: cover (Banner-Look), Focal wird separat übergeben. */
export const POST_SOCIAL_IMAGE_TRANSFORM = {
...SOCIAL_IMAGE_DIMENSIONS,
fit: "cover" as const,
format: "webp" as const,
};
+32
View File
@@ -255,3 +255,35 @@ export function warmResponsiveImage(
void fetchFn(u).catch(() => {});
}
}
/**
* Fire-and-forget Warmup für PostCard-Bilder (Grid: 400px base, 2x density, webp+jpeg).
* Spiegelt die URLs, die <RustyImage width={400} aspect="3/2"/> zur Laufzeit anfragt.
*/
export function warmPostCardImages(
posts: Array<{ _rawImageUrl?: string; _imageFocal?: { x: number; y: number } | null }>,
fetchFn: typeof fetch,
limit = 6,
): void {
const widths = [400, 800];
const formats: Array<'webp' | 'jpeg'> = ['webp', 'jpeg'];
for (const post of posts.slice(0, limit)) {
const raw = post._rawImageUrl;
if (!raw) continue;
const focal = post._imageFocal ?? undefined;
for (const w of widths) {
for (const format of formats) {
const url = getCmsImageUrl(raw, {
width: w,
height: Math.round((w / 3) * 2),
fit: 'cover',
format,
quality: 75,
ar: '3/2',
...(focal ? { focalX: focal.x, focalY: focal.y } : {}),
});
void fetchFn(url).catch(() => {});
}
}
}
}