feat(layout): add global announcement banner above nav
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:
@@ -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) */
|
||||
|
||||
@@ -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}
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
NavigationEntry,
|
||||
FooterEntry,
|
||||
PageConfigEntry,
|
||||
TopBannerEntry,
|
||||
} from '$lib/cms';
|
||||
import { ensureTransformedImage } from '$lib/rusty-image';
|
||||
import {
|
||||
@@ -84,6 +85,7 @@ export const load: LayoutServerLoad = async ({ locals, route }) => {
|
||||
let bootstrapNavSocial: NavigationEntry | null = null;
|
||||
let bootstrapFooter: FooterEntry | null = null;
|
||||
let bootstrapConfig: PageConfigEntry | null = null;
|
||||
let bootstrapTopBanner: TopBannerEntry | null = null;
|
||||
try {
|
||||
const batch = await batchFetch([
|
||||
{
|
||||
@@ -114,11 +116,17 @@ export const load: LayoutServerLoad = async ({ locals, route }) => {
|
||||
locale: 'de',
|
||||
resolve: 'logo',
|
||||
},
|
||||
{
|
||||
id: 'topBanner',
|
||||
collection: 'top_banner',
|
||||
slug: 'site-announcement',
|
||||
},
|
||||
]);
|
||||
bootstrapNavHeader = batchData<NavigationEntry>(batch.results.navHeader);
|
||||
bootstrapNavSocial = batchData<NavigationEntry>(batch.results.navSocial);
|
||||
bootstrapFooter = batchData<FooterEntry>(batch.results.footer);
|
||||
bootstrapConfig = batchData<PageConfigEntry>(batch.results.config);
|
||||
bootstrapTopBanner = batchData<TopBannerEntry>(batch.results.topBanner);
|
||||
} catch (err) {
|
||||
/* CMS nicht erreichbar — alle Felder bleiben null, Page rendert ohne. */
|
||||
logWarn('layout.bootstrap', err);
|
||||
@@ -328,5 +336,6 @@ export const load: LayoutServerLoad = async ({ locals, route }) => {
|
||||
seoDescriptionTemplate: pageConfig?.seoDescription ?? null,
|
||||
analyticsPm86SiteId:
|
||||
(pageConfig as { analyticsPm86SiteId?: string } | null)?.analyticsPm86SiteId ?? null,
|
||||
announcementBanner: bootstrapTopBanner,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import HeaderOverlay from '$lib/components/HeaderOverlay.svelte';
|
||||
import Footer from '$lib/components/Footer.svelte';
|
||||
import TopBanner from '$lib/components/TopBanner.svelte';
|
||||
import AnnouncementBanner from '$lib/components/AnnouncementBanner.svelte';
|
||||
import TranslationProvider from '$lib/components/TranslationProvider.svelte';
|
||||
import Breadcrumbs from '$lib/ui/Breadcrumbs.svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
@@ -291,6 +292,9 @@
|
||||
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-xs"
|
||||
>Zum Inhalt springen</a>
|
||||
<div class="flex min-h-screen flex-col text-stein-900">
|
||||
{#if data.announcementBanner}
|
||||
<AnnouncementBanner banner={data.announcementBanner} />
|
||||
{/if}
|
||||
{#if pageData?.preview || pageData?.previewDraft}
|
||||
<div
|
||||
class="sticky top-0 z-[90] w-full bg-erde-500 text-black shadow-sm"
|
||||
|
||||
Reference in New Issue
Block a user