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
+98 -7
View File
@@ -10,7 +10,7 @@
import Breadcrumbs from '$lib/ui/Breadcrumbs.svelte';
import type { LayoutData } from './$types';
import { ensureTransformedImage } from '$lib/rusty-image';
import { DEFAULT_SOCIAL_IMAGE_URL, SOCIAL_IMAGE_TRANSFORM } from '$lib/constants';
import { DEFAULT_SOCIAL_IMAGE_URL, SOCIAL_IMAGE_DIMENSIONS } from '$lib/constants';
let { data, children }: { data: LayoutData; children: import('svelte').Snippet } = $props();
@@ -19,6 +19,8 @@
seoTitle?: string;
seoDescription?: string;
socialImage?: string;
robots?: string;
jsonLd?: Record<string, unknown> | Record<string, unknown>[];
breadcrumbItems?: { href?: string; label: string }[];
topBanner?: {
banner: unknown;
@@ -59,17 +61,83 @@
});
const breadcrumbItems = $derived(pageData?.breadcrumbItems ?? []);
const robotsContent = $derived.by(() => {
const base = (pageData?.robots ?? 'index, follow').trim();
if (/noindex/i.test(base)) return base;
return `${base}, max-image-preview:large, max-snippet:-1`;
});
const topBannerData = $derived(pageData?.topBanner ?? null);
const bannerPreload = $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 };
});
function escapeLdJson(s: string): string {
return s.replace(/<\/script/gi, '<\\/script');
}
const jsonLdScript = $derived(
`<script type="application/ld+json">${JSON.stringify({
`<script type="application/ld+json">${escapeLdJson(JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebSite',
name: data.siteName,
url: baseUrl,
inLanguage: 'de-DE',
})}<\/script>`,
publisher: { '@type': 'Organization', name: data.siteName, ...(baseUrl ? { '@id': `${baseUrl}#org` } : {}) },
}))}<\/script>`,
);
const organizationLdScript = $derived.by(() => {
const sameAs = (data.socialLinks ?? [])
.map((l) => l.href)
.filter((h) => /^https?:\/\//.test(h));
const org: Record<string, unknown> = {
'@context': 'https://schema.org',
'@type': 'Organization',
...(baseUrl ? { '@id': `${baseUrl}#org` } : {}),
name: data.siteName,
url: baseUrl,
...(data.logoSocialImage ? { logo: data.logoSocialImage } : {}),
...(sameAs.length ? { sameAs } : {}),
};
return `<script type="application/ld+json">${escapeLdJson(JSON.stringify(org))}<\/script>`;
});
const pageJsonLdScripts = $derived.by(() => {
const raw = pageData?.jsonLd;
if (!raw) return '';
const items = Array.isArray(raw) ? raw : [raw];
return items
.map(
(o) =>
`<script type="application/ld+json">${escapeLdJson(JSON.stringify(o))}<\/script>`,
)
.join('');
});
const breadcrumbJsonLdScript = $derived.by(() => {
const items = pageData?.breadcrumbItems ?? [];
if (items.length < 2) return '';
const ld = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: items.map((item, i) => ({
'@type': 'ListItem',
position: i + 1,
name: item.label,
...(item.href
? {
item: item.href.startsWith('http')
? item.href
: `${baseUrl}${item.href}`,
}
: {}),
})),
};
return `<script type="application/ld+json">${escapeLdJson(JSON.stringify(ld))}<\/script>`;
});
const formbricksAppUrl = String(import.meta.env.PUBLIC_FORMBRICKS_APP_URL ?? '').replace(/\/$/, '');
const formbricksEnvId = String(import.meta.env.PUBLIC_FORMBRICKS_ENVIRONMENT_ID ?? '').trim();
@@ -81,6 +149,18 @@
<svelte:head>
<meta name="theme-color" content="#ffffff" />
<meta name="robots" content={robotsContent} />
{#if bannerPreload}
<link
rel="preload"
as="image"
href={bannerPreload.href}
imagesrcset={bannerPreload.srcset}
imagesizes="100vw"
type={bannerPreload.type}
fetchpriority="high"
/>
{/if}
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="alternate" type="application/rss+xml" title="Beiträge" href="/posts/rss.xml" />
<link rel="sitemap" href="/sitemap.xml" />
@@ -115,8 +195,8 @@
{/if}
{#if socialImageUrl}
<meta property="og:image" content={socialImageUrl} />
<meta property="og:image:width" content={String(SOCIAL_IMAGE_TRANSFORM.width)} />
<meta property="og:image:height" content={String(SOCIAL_IMAGE_TRANSFORM.height)} />
<meta property="og:image:width" content={String(SOCIAL_IMAGE_DIMENSIONS.width)} />
<meta property="og:image:height" content={String(SOCIAL_IMAGE_DIMENSIONS.height)} />
{/if}
<meta property="og:locale" content="de_DE" />
<meta property="og:site_name" content={data.siteName} />
@@ -130,8 +210,15 @@
{#if socialImageUrl}
<meta name="twitter:image" content={socialImageUrl} />
{/if}
<!-- JSON-LD WebSite -->
<!-- JSON-LD WebSite + Organization -->
{@html jsonLdScript}
{@html organizationLdScript}
{#if breadcrumbJsonLdScript}
{@html breadcrumbJsonLdScript}
{/if}
{#if pageJsonLdScripts}
{@html pageJsonLdScripts}
{/if}
{#if import.meta.env.PUBLIC_UMAMI_SCRIPT_URL && import.meta.env.PUBLIC_UMAMI_WEBSITE_ID}
<script
defer
@@ -152,6 +239,10 @@
</svelte:head>
<TranslationProvider translations={data.translations}>
<a
href="#main-content"
class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:bg-white focus:px-3 focus:py-2 focus:shadow focus:outline focus:outline-2 focus:outline-wald-600 focus:rounded-sm"
>Zum Inhalt springen</a>
<div class="flex min-h-screen flex-col text-zinc-900">
<Header
links={data.headerLinks}
@@ -173,7 +264,7 @@
/>
{/if}
<main class="flex-1 min-h-[60em]">
<main id="main-content" tabindex="-1" class="flex-1 min-h-[60em]">
<div class="container-custom pb-12">
{#if breadcrumbItems.length > 0}
<Breadcrumbs items={breadcrumbItems} />