diff --git a/README.md b/README.md
index 4842263..5db54da 100644
--- a/README.md
+++ b/README.md
@@ -1854,6 +1854,31 @@ has its own dedicated Live Preview renderer, not the shared generic
order-status one — there's no order for this email type, so no fake
"Bestellnummer" line; CTA is "Zum Produkt", not "Bestellung ansehen".
+## Google Maps (consent-gated embed, prepared but unused)
+
+No map is embedded anywhere in this app yet — `GoogleMapsEmbed.tsx`
+exists as ready-to-use prep work. Unlike GA/Facebook Pixel/GTM (scripts
+this app injects itself, see the tracking-codes section above), an
+embedded iframe is gated by **Klaro's own built-in contextual-consent
+mechanism**, not `loadTrackingCode.ts`: the iframe renders with its real
+`src` and a `data-name="google-maps"` attribute; Klaro's DOM scan
+(`renderContextualConsentNotices`, runs as part of `Klaro.render()`)
+finds it after mount, blanks `src` and swaps in its own "Karte laden?"
+placeholder (styled to match this site via `KlaroConsentManager.tsx`'s
+`KlaroTheme`) until that service's consent is granted, then restores the
+real `src`. `loadTrackingCode.ts`'s `google-maps` case is a deliberate
+no-op — there's nothing to inject, Klaro handles the swap entirely on its
+own.
+
+Backend prerequisite: exactly one `tracking-codes` row with
+`provider: google-maps` must exist and be `active` (see the Payload
+README) — `klaroConfig.ts`'s `serviceName()` special-cases this provider
+to the fixed name `"google-maps"` (not id-based like every other
+provider) since there's no distinguishing ID the way GA/FB/GTM each have
+one; a second `google-maps` row would collide on that name. To actually
+use it: ``
+anywhere.
+
## Product image gallery
`ProductGallery.tsx` — main image + thumbnail strip, swappable on click,
diff --git a/app/components/GoogleMapsEmbed.tsx b/app/components/GoogleMapsEmbed.tsx
new file mode 100644
index 0000000..348e756
--- /dev/null
+++ b/app/components/GoogleMapsEmbed.tsx
@@ -0,0 +1,44 @@
+// Server Component — no "use client" needed. The consent gating itself
+// happens entirely client-side inside Klaro (KlaroConsentManager.tsx),
+// not via any React state here: Klaro's own contextual-consent DOM scan
+// (renderContextualConsentNotices, runs as part of Klaro.render()) finds
+// this iframe by its `data-name="google-maps"` attribute after mount,
+// blanks its `src` and inserts its own "Karte laden?" placeholder in
+// front of it until that service's consent is granted, then restores the
+// real `src`. See klaroConfig.ts's own comment on the "google-maps"
+// service this depends on — a `tracking-codes` row with that provider
+// must exist and be `active`, or Klaro never gates (and never un-blanks)
+// this iframe at all.
+//
+// `width`/`height` are real HTML attributes, not just the Tailwind aspect-
+// ratio wrapper below — Klaro reads `element.width`/`element.height` (the
+// DOM attributes) to size its own placeholder box before the real iframe
+// has rendered, so both need to be present even though the wrapper's
+// `aspect-[...]` class is what actually controls layout.
+export function GoogleMapsEmbed({
+ src,
+ title,
+ className,
+}: {
+ /** A full Google Maps embed URL (Google Maps → Teilen → Karte einbetten
+ * → "HTML kopieren" → the `src` attribute of that iframe), e.g.
+ * "https://www.google.com/maps/embed?pb=...". */
+ src: string;
+ title: string;
+ className?: string;
+}) {
+ return (
+
+
+
+ );
+}
diff --git a/app/components/KlaroConsentManager.tsx b/app/components/KlaroConsentManager.tsx
index c056ff6..fc523da 100644
--- a/app/components/KlaroConsentManager.tsx
+++ b/app/components/KlaroConsentManager.tsx
@@ -73,6 +73,30 @@ function KlaroTheme() {
.klaro .cm-btn:hover { opacity: 0.88; }
.klaro .cm-btn:active { transform: scale(0.97); }
.klaro .cm-btn:focus-visible { outline: 2px solid #f6a701 !important; outline-offset: 2px; }
+ /* The "Karte laden?" placeholder ContextualConsentNotice renders in
+ place of a gated embed (GoogleMapsEmbed.tsx) — fills that embed's
+ own aspect-ratio box, so this needs to look like a real card
+ slot, not a bare unstyled div floating inside it. */
+ .klaro.cm-as-context-notice {
+ height: 100% !important;
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ }
+ .klaro .context-notice {
+ width: 100% !important;
+ height: 100% !important;
+ display: flex !important;
+ flex-direction: column !important;
+ align-items: center !important;
+ justify-content: center !important;
+ gap: 4px !important;
+ padding: 24px !important;
+ background: #fffdf8 !important;
+ text-align: center !important;
+ }
+ .klaro .context-notice p { text-align: center !important; max-width: 32ch; }
+ .klaro .context-notice .cm-buttons { display: flex !important; gap: 10px !important; margin-top: 6px !important; }
.klaro a.cm-link, .klaro .cookie-notice a, .klaro .cookie-modal a {
color: #a06b00 !important;
text-decoration: underline !important;
diff --git a/app/lib/klaroConfig.ts b/app/lib/klaroConfig.ts
index f49fc5e..84e08c0 100644
--- a/app/lib/klaroConfig.ts
+++ b/app/lib/klaroConfig.ts
@@ -28,7 +28,16 @@ export type KlaroConfig = {
}[];
};
+// "google-maps" is a fixed name, not id-based like every other provider —
+// GoogleMapsEmbed.tsx has to hardcode a `data-name` matching this exact
+// string for Klaro's own contextual-consent DOM scan
+// (renderContextualConsentNotices in node_modules/klaro) to find it, and
+// there's no per-row distinguishing ID the way GA/FB/GTM each have one
+// (a Maps embed URL is per-page, not stored on this row at all — see
+// TrackingCodes.ts's own field comment). Only ever create ONE
+// google-maps row in the admin; a second one would collide on this name.
export function serviceName(code: TrackingCode): string {
+ if (code.provider === "google-maps") return "google-maps";
return `tracking-code-${code.id}`;
}
@@ -40,6 +49,8 @@ function providerTitle(code: TrackingCode): string {
return "Facebook Pixel";
case "google-tag-manager":
return "Google Tag Manager";
+ case "google-maps":
+ return "Google Maps";
default:
return "Sonstiges Tracking-Skript";
}
@@ -116,6 +127,7 @@ export function buildKlaroConfig(codes: TrackingCode[], onAccept: (code: Trackin
necessary: "Notwendig",
analytics: "Analyse",
marketing: "Marketing",
+ content: "Inhalte",
},
...serviceTranslations,
},
@@ -126,6 +138,15 @@ export function buildKlaroConfig(codes: TrackingCode[], onAccept: (code: Trackin
purposes: [code.consentCategory],
required: code.consentCategory === "necessary",
default: code.consentCategory === "necessary",
+ // For "google-maps", `onAccept` below is a no-op (see
+ // loadTrackingCode.ts) — an embed's src isn't a script this app
+ // injects, Klaro's OWN contextual-consent DOM scan
+ // (renderContextualConsentNotices, runs as part of Klaro.render())
+ // finds any `data-name="google-maps"` iframe on the page and
+ // blanks/restores its `src` itself, purely by service name +
+ // consent state. This service entry's only job is telling Klaro
+ // that name exists and which purpose gates it.
+ //
// A restock/page-reload shouldn't re-fire the loader for a service
// the visitor already accepted in an earlier session within this
// same page load — Klaro still calls `callback` once per accepted
diff --git a/app/lib/loadTrackingCode.ts b/app/lib/loadTrackingCode.ts
index 20d20ec..7e3c656 100644
--- a/app/lib/loadTrackingCode.ts
+++ b/app/lib/loadTrackingCode.ts
@@ -23,6 +23,12 @@ function injectScript(id: string, src?: string, inlineJs?: string) {
// only one whose content is admin-supplied rather than a fixed snippet
// (trusted deliberately, see TrackingCodes.ts on the backend).
export function loadTrackingCode(code: TrackingCode): void {
+ // No-op — Google Maps isn't a script this app injects. Klaro's own
+ // contextual-consent DOM scan handles any `data-name="google-maps"`
+ // iframe (GoogleMapsEmbed.tsx) independently, purely off the service's
+ // consent state — see klaroConfig.ts's own comment on this service.
+ if (code.provider === "google-maps") return;
+
const baseId = `tracking-code-${code.id}`;
if (code.provider === "google-analytics" && code.measurementId) {
diff --git a/app/lib/payload.ts b/app/lib/payload.ts
index 8a738ca..8dbe8e7 100644
--- a/app/lib/payload.ts
+++ b/app/lib/payload.ts
@@ -1181,8 +1181,8 @@ export async function getSeoSettings(): Promise {
export type TrackingCode = {
id: number;
- provider: "google-analytics" | "facebook-pixel" | "google-tag-manager" | "other";
- consentCategory: "necessary" | "analytics" | "marketing";
+ provider: "google-analytics" | "facebook-pixel" | "google-tag-manager" | "google-maps" | "other";
+ consentCategory: "necessary" | "analytics" | "marketing" | "content";
measurementId: string | null;
pixelId: string | null;
containerId: string | null;