Replace custom cookie banner with Klaro (open-source, self-hosted CMP)

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>
This commit is contained in:
Marco
2026-08-01 20:33:10 +00:00
parent 259002f5c0
commit 537543bf91
8 changed files with 249 additions and 195 deletions
+45
View File
@@ -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;
}