diff --git a/app/components/CookieBanner.tsx b/app/components/CookieBanner.tsx
deleted file mode 100644
index f2cf92f..0000000
--- a/app/components/CookieBanner.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-"use client";
-
-import Link from "next/link";
-import { AnimatePresence, motion } from "motion/react";
-import { useConsent } from "../lib/useConsent";
-
-/**
- * Bottom banner, shown whenever useConsent()'s cookie hasn't recorded a
- * decision yet — the hard prerequisite for TrackingScripts.tsx to ever
- * render an analytics/marketing script (see that file's own comment).
- * Two equally-sized buttons, not a prominent "Akzeptieren" next to a
- * de-emphasized reject link — TTDSG requires an equally easy way to
- * decline, not just technically present a way to.
- */
-export function CookieBanner() {
- const { consent, loaded, setConsent } = useConsent();
- const visible = loaded && consent === null;
-
- return (
-
- {visible && (
-
-
-
- Wir nutzen Cookies, um diese Seite zu betreiben und ihre Nutzung zu verstehen. Mehr dazu in unserer{" "}
-
- Datenschutzerklärung
-
- .
-
-
-
-
-
-
-
- )}
-
- );
-}
diff --git a/app/components/KlaroConsentManager.tsx b/app/components/KlaroConsentManager.tsx
new file mode 100644
index 0000000..d2426a7
--- /dev/null
+++ b/app/components/KlaroConsentManager.tsx
@@ -0,0 +1,45 @@
+"use client";
+
+import { useEffect } from "react";
+import "klaro/dist/klaro.css";
+import type { TrackingCode } from "../lib/payload";
+import { buildKlaroConfig } from "../lib/klaroConfig";
+import { loadTrackingCode } from "../lib/loadTrackingCode";
+
+// Replaced a hand-rolled CookieBanner.tsx + useConsent.ts + TrackingScripts.tsx
+// with kiprotect/klaro (open source, self-hosted, npm install klaro) —
+// the custom banner only ever covered the accept/reject UI itself; Klaro
+// additionally brings a real per-service consent list, bundled German UI
+// translations, and (via `cookies`, not used here yet) cookie-deletion on
+// withdrawal — all things a hand-rolled version would have had to build
+// from scratch. See project memory for the fuller reasoning.
+//
+// Dynamically imported inside an effect (client-only, after mount) rather
+// than a static top-level import — Klaro touches `window`/`document` at
+// module-eval time in places, which isn't SSR-safe. The CSS import above
+// stays static (Next.js requires CSS imports to be static, not inside a
+// dynamic import()), paired with the "-no-css" JS build so the stylesheet
+// isn't loaded twice.
+export function KlaroConsentManager({ codes }: { codes: TrackingCode[] }) {
+ useEffect(() => {
+ // Nothing to ask consent for — don't even load/render Klaro. A cookie
+ // banner with zero services to list would just be visual noise.
+ if (codes.length === 0) return;
+
+ let cancelled = false;
+ import("klaro/dist/klaro-no-css").then((Klaro) => {
+ if (cancelled) return;
+ const config = buildKlaroConfig(codes, loadTrackingCode);
+ Klaro.setup(config);
+ });
+ return () => {
+ cancelled = true;
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- `codes` comes
+ // from a server-fetched, 60s-ISR-cached layout prop; it's stable for
+ // the lifetime of this component in practice, and Klaro.setup() isn't
+ // meant to be called more than once per page load anyway.
+ }, []);
+
+ return null;
+}
diff --git a/app/components/TrackingScripts.tsx b/app/components/TrackingScripts.tsx
deleted file mode 100644
index 40d30d5..0000000
--- a/app/components/TrackingScripts.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-"use client";
-
-import Script from "next/script";
-import { useConsent } from "../lib/useConsent";
-import type { TrackingCode } from "../lib/payload";
-
-// Renders one next/script per active tracking code, but only once consent
-// actually allows it — `necessary` always renders (nothing in that
-// category exists yet, but the field exists for future use, e.g. a
-// consent-management/CMP script itself); `analytics`/`marketing` both gate
-// on the same single useConsent() flag (see that hook's own comment on why
-// there's no separate marketing toggle yet). Fetched server-side
-// (getTrackingCodes(), passed in as a prop from layout.tsx) since this
-// component itself is a client component and can't call that fetcher
-// directly without losing the 60s ISR cache.
-export function TrackingScripts({ codes }: { codes: TrackingCode[] }) {
- const { consent, loaded } = useConsent();
- if (!loaded) return null;
-
- const allowed = codes.filter((code) => code.consentCategory === "necessary" || consent?.analytics === true);
-
- return (
- <>
- {allowed.map((code) => {
- if (code.provider === "google-analytics" && code.measurementId) {
- return (
-
-
-
-
- );
- }
- if (code.provider === "facebook-pixel" && code.pixelId) {
- return (
-
- );
- }
- if (code.provider === "google-tag-manager" && code.containerId) {
- return (
-
- );
- }
- // 'other' — the only provider whose script content is admin-
- // supplied rather than a fixed loader snippet. Trusted deliberately
- // (see TrackingCodes.ts's own admin.description) — only reachable
- // by someone with backend admin access in the first place.
- if (code.provider === "other" && code.customScript) {
- return ;
- }
- return null;
- })}
- >
- );
-}
diff --git a/app/layout.tsx b/app/layout.tsx
index 6d923cb..cc45144 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -4,8 +4,7 @@ import "./globals.css";
import { Navbar } from "./components/Navbar";
import { CartFlyProvider } from "./components/CartFly";
import { CartSync } from "./components/CartSync";
-import { CookieBanner } from "./components/CookieBanner";
-import { TrackingScripts } from "./components/TrackingScripts";
+import { KlaroConsentManager } from "./components/KlaroConsentManager";
import { getProducts, getSeoSettings, getCompanySettings, getWishlistEnabled, getSearchEnabled, getTrackingCodes } from "./lib/payload";
import { buildOrganizationSchema } from "./lib/structuredData";
@@ -93,13 +92,12 @@ export default async function RootLayout({
>
-
+
{children}
-