feat(layout): add global announcement banner above nav
Deploy / verify (push) Successful in 1m12s
Deploy / deploy (push) Successful in 1m6s

Adds AnnouncementBanner component fed by a new top_banner CMS entry
(site-announcement). Extended top_banner schema and TS types with
optional link + linkLabel fields. Rendered inline-markdown text.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-05-08 21:56:42 +02:00
parent 8db68e073c
commit ef163fb336
5 changed files with 70 additions and 0 deletions
+8
View File
@@ -5446,6 +5446,10 @@ export interface components {
id: string;
/** @description Banner text (e.g. notice at top of page) */
text: string;
/** @description Optional URL the banner links to */
link?: string;
/** @description CTA label for the link */
linkLabel?: string;
};
top_banner_input: {
/** @description URL slug (used as filename) */
@@ -5454,6 +5458,10 @@ export interface components {
id: string;
/** @description Banner text (e.g. notice at top of page) */
text: string;
/** @description Optional URL the banner links to */
link?: string;
/** @description CTA label for the link */
linkLabel?: string;
};
translation_bundle: {
/** @description Entry identifier (filename) */
+25
View File
@@ -747,6 +747,31 @@ export async function getTextFragmentBySlug(
}
/** Fullwidth-Banner (z. B. für Top-Banner mit Bild). */
export type TopBannerEntry = components["schemas"]["top_banner"];
/** Top-Banner anhand Slug (GET /api/content/top_banner/:slug). */
export async function getTopBannerBySlug(
slug: string,
options?: { locale?: string },
): Promise<TopBannerEntry | null> {
const key = `top_banner:slug:${slug}:${options?.locale ?? ""}`;
return cached("top_banner", key, async () => {
const base = getBaseUrl();
const url = new URL(
`${base}/api/content/top_banner/${encodeURIComponent(slug)}`,
);
if (options?.locale) url.searchParams.set("_locale", options.locale);
const res = await cmsFetch(url.toString());
if (res.status === 404) return null;
if (!res.ok) {
throw new Error(
`RustyCMS get top_banner failed: ${res.status} for slug "${slug}".`,
);
}
return (await res.json()) as TopBannerEntry;
});
}
export type FullwidthBannerEntry = components["schemas"]["fullwidth_banner"];
/** Fullwidth-Banner anhand Slug (GET /api/content/fullwidth_banner/:slug). */
@@ -0,0 +1,24 @@
<script lang="ts">
import type { TopBannerEntry } from '$lib/cms';
import { marked } from '$lib/markdown-safe';
let { banner }: { banner: TopBannerEntry } = $props();
const textHtml = $derived(banner?.text ? marked.parseInline(banner.text) as string : '');
</script>
{#if banner?.text}
<div class="w-full bg-wald-600 text-white print:hidden" role="note" style="font-size: 0.7rem;">
<div class="container-custom flex items-center gap-3 py-2 md:justify-center md:text-center">
<span>{@html textHtml}</span>
{#if banner.link}
<a
href={banner.link}
class="shrink-0 font-semibold underline underline-offset-2 hover:no-underline"
>
{banner.linkLabel ?? 'Mehr erfahren'}
</a>
{/if}
</div>
</div>
{/if}