Update project configuration and enhance translation support. Added caching mechanism for CMS responses with a new script, updated .gitignore to include cache files, and introduced translation handling in various components. Enhanced package.json with new scripts for cache warming and deployment. Added new translation-related components and improved existing ones for better localization support.

This commit is contained in:
Peter Meier
2026-03-17 22:32:31 +01:00
parent 1f5454d04e
commit daa4ea17b1
46 changed files with 5207 additions and 781 deletions
+92 -42
View File
@@ -23,6 +23,10 @@ import {
SITE_NAME,
SOCIAL_IMAGE_TRANSFORM,
} from "../lib/constants";
import TranslationProvider from "../components/TranslationProvider.svelte";
import { t, T } from "../lib/translations";
import { getCmsFromCache } from "../lib/cms-cache-state";
import type { TranslationKey } from "../lib/translations";
import { marked } from "marked";
marked.setOptions({ gfm: true });
@@ -47,8 +51,8 @@ interface Props {
showBannerInLayout?: boolean;
/** Optional: absoluter oder relativer URL für og:image / twitter:image (z. B. Post-Bild). */
image?: string | null;
/** Optional: Breadcrumb-Items. Letzter Eintrag ohne href = aktuelle Seite. */
breadcrumbItems?: { href?: string; label: string }[];
/** Optional: Breadcrumb-Items. Letzter Eintrag ohne href = aktuelle Seite. translationKey überschreibt label (übersetzt). */
breadcrumbItems?: { href?: string; label?: string; translationKey?: TranslationKey }[];
}
const {
@@ -62,6 +66,21 @@ const {
breadcrumbItems = undefined,
} = Astro.props;
// Übersetzungen aus Middleware (Astro.locals.translations) für TranslationProvider und Breadcrumbs
const translations: Record<string, string> = Astro.locals.translations ?? {};
/** JSON für script type="application/json": </ escapen, damit das Tag nicht geschlossen wird. */
function safeJsonScriptContent(obj: Record<string, string>): string {
return JSON.stringify(obj).replace(/<\//g, "\\u003c/");
}
const resolvedBreadcrumbItems =
breadcrumbItems?.map((item) => ({
href: item.href,
label:
item.translationKey != null
? t(translations, item.translationKey)
: (item.label ?? ""),
})) ?? [];
// Header: Navigation laden und Links bauen
interface NavLink {
href: string;
@@ -251,7 +270,8 @@ const baseUrl = isLocalhost
const pathname = Astro.url.pathname || "/";
const canonical = `${baseUrl}${pathname}`;
// Social-Image immer über RustyImage (externe URLs transformieren; lokale Pfade unverändert nutzen)
// Social-Image immer über RustyImage (externe URLs transformieren; lokale Pfade unverändert nutzen).
// Fallback: Wenn kein Bild/Logo und Transform fehlschlägt → DEFAULT_SOCIAL_IMAGE_URL (absolut), damit WhatsApp/og immer ein Vorschaubild hat.
const socialImageSource = imageProp ?? logoUrl ?? null;
const needsTransform =
!socialImageSource ||
@@ -275,7 +295,9 @@ if (needsTransform) {
// Bereits lokaler Pfad (z. B. von Post), nur baseUrl davor
socialImageResolved = `${baseUrl}${urlToTransform.startsWith("/") ? "" : "/"}${urlToTransform}`;
}
const socialImage = socialImageResolved ?? "";
const socialImage =
socialImageResolved ??
(urlToTransform.startsWith("http") ? urlToTransform : "");
const siteName =
(import.meta.env.PUBLIC_SITE_NAME as string | undefined) || SITE_NAME;
@@ -342,15 +364,19 @@ const titleSubheadlineHtml =
showTitleInContent && subheadline?.trim()
? (marked.parseInline(subheadline) as string)
: "";
const cmsFromCache = getCmsFromCache();
---
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width" />
<meta name="theme-color" content="#2d7a45" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" href="/apple-touch-icon.svg" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#2d7a45" />
<meta name="msapplication-TileColor" content="#2d7a45" />
<meta name="generator" content={Astro.generator} />
<title>{seoTitle}</title>
{seoDescription && <meta name="description" content={seoDescription} />}
@@ -411,47 +437,71 @@ const titleSubheadlineHtml =
/>
)
}
<script
src="https://analytics.pm86.de/api/script.js"
data-site-id="ad2858cf2db1"
defer
/>
</head>
<body class="flex min-h-screen flex-col text-zinc-900">
<Header
links={headerLinks}
socialLinks={socialLinks}
logoUrl={logoUrl}
client:load
/>
<HeaderOverlay client:load />
{
showBannerInLayout &&
(topBanner != null || topBannerResolvedImages.length > 0) && (
<TopBanner
banner={topBanner}
resolvedImages={topBannerResolvedImages}
headline={headline}
subheadline={subheadline}
/>
)
import.meta.env.DEV && cmsFromCache && (
<div
class="sticky top-0 z-50 bg-amber-500 text-amber-950 text-center py-1.5 px-2 text-sm font-medium"
role="status"
aria-live="polite"
>
CMS nicht erreichbar Daten werden aus dem Cache (.cache/cms) geladen.
</div>
)
}
<main class="flex-1 min-h-[60em]">
<div class="container-custom pb-12">
{breadcrumbItems && breadcrumbItems.length > 0 && (
<Breadcrumbs items={breadcrumbItems} client:load />
)}
{
showTitleInContent && (titleHeadlineHtml || titleSubheadlineHtml) && (
<div class="mb-6 pt-10 pageTitle">
{titleHeadlineHtml && (
<h1 class="pageTitle" set:html={titleHeadlineHtml} />
)}
{titleSubheadlineHtml && (
<h2 class="pageTitle" set:html={titleSubheadlineHtml} />
)}
</div>
<script id="astro-translations" type="application/json" set:html={safeJsonScriptContent(translations)}></script>
<script define:vars={{ translations }}>
try { window.__TRANSLATIONS__ = typeof translations !== "undefined" && translations != null ? translations : {}; } catch (e) {}
</script>
<TranslationProvider translations={translations}>
<Header
links={headerLinks}
socialLinks={socialLinks}
logoUrl={logoUrl}
translations={translations}
client:load
/>
<HeaderOverlay client:load />
{
showBannerInLayout &&
(topBanner != null || topBannerResolvedImages.length > 0) && (
<TopBanner
banner={topBanner}
resolvedImages={topBannerResolvedImages}
headline={headline}
subheadline={subheadline}
/>
)
}
<slot />
</div>
</main>
<Footer footer={footerData} />
}
<main class="flex-1 min-h-[60em]">
<div class="container-custom pb-12">
{resolvedBreadcrumbItems.length > 0 && (
<Breadcrumbs items={resolvedBreadcrumbItems} client:load />
)}
{
showTitleInContent &&
(titleHeadlineHtml || titleSubheadlineHtml) && (
<div class="mb-6 pt-10 pageTitle">
{titleHeadlineHtml && (
<h1 class="pageTitle" set:html={titleHeadlineHtml} />
)}
{titleSubheadlineHtml && (
<h2 class="pageTitle" set:html={titleSubheadlineHtml} />
)}
</div>
)
}
<slot />
</div>
</main>
<Footer footer={footerData} translations={translations} />
</TranslationProvider>
<script src="/pagefind/pagefind-ui.js" is:inline></script>
</body>
</html>