537543bf91
Swaps the hand-rolled CookieBanner.tsx/useConsent.ts/TrackingScripts.tsx for kiprotect/klaro — brings a real per-service consent list and bundled German UI translations that a custom implementation would have had to build from scratch (per user feedback that a proper CMP is worth it over a purely custom binary accept/reject banner). klaroConfig.ts builds Klaro's config dynamically from the existing tracking-codes backend collection (one Klaro "service" per row, grouped by consentCategory as its purpose). loadTrackingCode.ts is the actual script-injection side effect, wired in via each service's `callback(consent)` — same GA4/Facebook-Pixel/GTM/custom loader logic TrackingScripts.tsx had, just triggered imperatively instead of declaratively rendered. Brand color applied via Klaro's CSS custom property overrides (styling.green1 etc.), not custom SCSS. No @types/klaro package exists — types/klaro.d.ts declares only the small surface actually used. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
105 lines
3.8 KiB
TypeScript
105 lines
3.8 KiB
TypeScript
import type { Metadata } from "next";
|
|
import { Inter, Playfair_Display, Caveat, Lora } from "next/font/google";
|
|
import "./globals.css";
|
|
import { Navbar } from "./components/Navbar";
|
|
import { CartFlyProvider } from "./components/CartFly";
|
|
import { CartSync } from "./components/CartSync";
|
|
import { KlaroConsentManager } from "./components/KlaroConsentManager";
|
|
import { getProducts, getSeoSettings, getCompanySettings, getWishlistEnabled, getSearchEnabled, getTrackingCodes } from "./lib/payload";
|
|
import { buildOrganizationSchema } from "./lib/structuredData";
|
|
|
|
const inter = Inter({
|
|
variable: "--font-inter",
|
|
subsets: ["latin"],
|
|
weight: ["400", "500", "600", "700"],
|
|
});
|
|
|
|
const playfair = Playfair_Display({
|
|
variable: "--font-playfair",
|
|
subsets: ["latin"],
|
|
weight: ["600", "700", "800"],
|
|
});
|
|
|
|
const caveat = Caveat({
|
|
variable: "--font-caveat",
|
|
subsets: ["latin"],
|
|
weight: ["700"],
|
|
});
|
|
|
|
const lora = Lora({
|
|
variable: "--font-lora",
|
|
subsets: ["latin"],
|
|
weight: ["400", "600"],
|
|
});
|
|
|
|
// Backend-driven since 2026-07-24 (CompanySettings' "SEO" tab) — the
|
|
// literal strings below are only the fallback getSeoSettings() returns if
|
|
// that field is empty or unreachable, kept identical to what used to be
|
|
// hardcoded here so nothing changes until an admin actually fills in the
|
|
// new fields.
|
|
export async function generateMetadata(): Promise<Metadata> {
|
|
const seo = await getSeoSettings();
|
|
return {
|
|
metadataBase: new URL("https://einfach-produktiv.mk360.de"),
|
|
title: {
|
|
default: seo.defaultTitle ?? "einfach produktiv.",
|
|
template: seo.titleTemplate ?? "%s | einfach produktiv.",
|
|
},
|
|
description: seo.defaultDescription ?? undefined,
|
|
openGraph: {
|
|
siteName: "einfach produktiv.",
|
|
locale: "de_DE",
|
|
type: "website",
|
|
images: seo.defaultOgImage ? [{ url: seo.defaultOgImage }] : undefined,
|
|
},
|
|
twitter: {
|
|
card: "summary_large_image",
|
|
images: seo.defaultOgImage ? [seo.defaultOgImage] : undefined,
|
|
},
|
|
};
|
|
}
|
|
|
|
export default async function RootLayout({
|
|
children,
|
|
}: Readonly<{
|
|
children: React.ReactNode;
|
|
}>) {
|
|
// Same 60s-ISR-cached call every other page already makes — reused here
|
|
// just to know whether Navbar's "Shop" link should behave as an anchor
|
|
// to the homepage spotlight instead of a real /shop navigation (see
|
|
// Navbar.tsx/ProductSpotlight.tsx).
|
|
const [products, seller, wishlistEnabled, searchEnabled, trackingCodes] = await Promise.all([
|
|
getProducts(),
|
|
getCompanySettings(),
|
|
getWishlistEnabled(),
|
|
getSearchEnabled(),
|
|
getTrackingCodes(),
|
|
]);
|
|
const singleActiveProduct = products.filter((p) => p.active).length === 1;
|
|
// Organization JSON-LD on every page — one canonical node (@id) that
|
|
// Product/Article schemas elsewhere link back to via `{ "@id": ... }`
|
|
// instead of repeating the full seller object per page (see
|
|
// structuredData.ts's own comment). getCompanySettings() is already the
|
|
// established pattern for a public page needing seller data server-side
|
|
// (see /impressum) — only non-sensitive fields (name/address/email/
|
|
// vatID) ever make it into the rendered schema, never iban/bic.
|
|
const organizationSchema = buildOrganizationSchema(seller);
|
|
|
|
return (
|
|
<html
|
|
lang="de"
|
|
className={`${inter.variable} ${playfair.variable} ${caveat.variable} ${lora.variable} h-full antialiased scroll-smooth`}
|
|
>
|
|
<body className="min-h-full flex flex-col">
|
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema) }} />
|
|
<KlaroConsentManager codes={trackingCodes} />
|
|
<CartFlyProvider>
|
|
<CartSync />
|
|
<Navbar singleActiveProduct={singleActiveProduct} wishlistEnabled={wishlistEnabled} searchEnabled={searchEnabled} />
|
|
{children}
|
|
</CartFlyProvider>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|