Google Maps DSGVO-konform einbetten — Consent-Box mit App-Links


Gleiche Ausgangslage wie beim YouTube-Embed: Ein normaler <iframe src="google.com/maps/embed/…"> schickt beim Seitenaufruf sofort Daten an Google — IP-Adresse, Browser-Fingerprint, Cookies — ohne dass der Nutzer zugestimmt hat. Für eine öffentlich zugängliche Website ein DSGVO-Problem.

Was ich gebraucht habe:

  1. Vor dem Klick: null Requests an Google. Keine Karte, kein Tile, gar nichts.
  2. Echte Einwilligung — Klick auf „Einmalig anzeigen” oder „Zustimmen und merken” ist die Zustimmung (Art. 6 Abs. 1 lit. a DSGVO).
  3. App-Links — wer die Karte lieber direkt in Google Maps oder Apple Maps öffnet, kann das ohne den iframe.

Die Komponente

src/components/GoogleMapsEmbed.astro — Verwendung so einfach wie möglich:

<GoogleMapsEmbed
  lat={7.8807048}
  lng={98.3657287}
  title="Naka Weekend Market, Phuket"
/>

Das war’s. src, href und zoom (Standard: 15) sind optional — die Komponente baut die Embed-URL und alle App-Links automatisch aus den Koordinaten.

Props

PropTypPflichtBeschreibung
latnumber✓ (oder src)Breitengrad
lngnumber✓ (oder src)Längengrad
titlestringOrtsname (Beschriftung + App-Links)
srcstring✓ (oder lat+lng)Manuell gebaute Embed-URL
hrefstringÜberschreibt den Google-Maps-Link
zoomnumberZoom-Level (Standard: 15)

Kern-Template

<figure class="maps-embed">
  {title && <figcaption class="maps-caption">{title}</figcaption>}
  <div class="maps-consent" data-src={src} data-title={title ?? ''}>
    <h3>{t(locale, 'maps.externalContent')}</h3>
    <p>{t(locale, 'maps.description')}</p>
    <p>{t(locale, 'maps.serviceName')}</p>
    <div class="maps-consent-buttons">
      <button data-remember="true">{t(locale, 'maps.loadMapRemember')}</button>
      <button>{t(locale, 'maps.loadMap')}</button>
    </div>
    <p class="maps-consent-footer">
      {t(locale, 'maps.consentFooter')}
      <a href={privacyHref}>{t(locale, 'maps.privacyLink')}</a>.
    </p>
  </div>
  <div class="maps-actions">
    <a href={googleMapsHref}>📍 Google Maps</a>
    {appleMapsHref && <a href={appleMapsHref}>📍 Apple Maps</a>}
  </div>
</figure>

Die maps-actions-Leiste mit den App-Links liegt außerhalb von .maps-consent — sie ist immer sichtbar, egal ob die Karte geladen ist oder nicht.

Script

<script>
  const CONSENT_KEY = 'gmaps-consent';

  function buildIframe(src, title) {
    const iframe = document.createElement('iframe');
    iframe.src = src;
    iframe.allow = 'fullscreen';
    iframe.loading = 'lazy';
    iframe.referrerPolicy = 'no-referrer-when-downgrade';
    iframe.style.cssText = 'width:100%;aspect-ratio:4/3;border:0;display:block';
    if (title) iframe.title = title;
    return iframe;
  }

  const hasConsent = (() => {
    try { return localStorage.getItem(CONSENT_KEY) === '1'; } catch { return false; }
  })();

  document.querySelectorAll('.maps-consent').forEach((container) => {
    const { src, title } = container.dataset;
    if (!src) return;
    if (hasConsent) { container.replaceWith(buildIframe(src, title)); return; }
    container.querySelectorAll('button').forEach((btn) => {
      btn.addEventListener('click', () => {
        if (btn.dataset.remember === 'true') {
          try { localStorage.setItem(CONSENT_KEY, '1'); } catch {}
        }
        container.replaceWith(buildIframe(src, title));
      });
    });
  });
</script>

Zwei Unterschiede zum YouTube-Script:

  • referrerPolicy: 'no-referrer-when-downgrade' statt strict-origin-when-cross-origin — Google Maps braucht den vollen Referrer (Protokoll + Domain), schlägt aber nicht wegen fehlender Pfade fehl wie YouTube.
  • Eigener CONSENT_KEY (gmaps-consent) — getrennt von yt-consent, weil es sich um einen anderen Dienst handelt. Wer YouTube freigegeben hat, muss für Maps separat zustimmen.

Koordinaten-Logik

// Auto-build src from coordinates if not provided
if (!src && lat != null && lng != null) {
  src = `https://maps.google.com/maps?q=${lat},${lng}&output=embed&zoom=${zoom}`;
}

// Apple Maps URL
const appleMapsHref = lat && lng
  ? `https://maps.apple.com/?ll=${lat},${lng}&q=${encodeURIComponent(title ?? '')}`
  : title
    ? `https://maps.apple.com/?q=${encodeURIComponent(title)}`
    : null;

Wenn keine expliziten lat/lng Props übergeben werden, parst die Komponente die Koordinaten aus der src-URL (Format ?q=lat,lng) nach.

i18n

Neue Keys in src/i18n/ui.ts:

'maps.externalContent': 'Externer Inhalt',
'maps.description': 'An dieser Stelle findest du eine interaktive Karte von Google Maps.',
'maps.serviceName': 'Google Maps (maps.google.com)',
'maps.loadMap': 'Einmalig anzeigen',
'maps.loadMapRemember': 'Zustimmen und merken',
'maps.openInMaps': 'In Google Maps öffnen',
'maps.consentFooter': 'Ich bin damit einverstanden, …',
'maps.privacyLink': 'Datenschutzerklärung',

CSP anpassen

Google Maps benötigt zwei zusätzliche Einträge in frame-src:

- frame-src https://www.youtube-nocookie.com;
+ frame-src https://www.youtube-nocookie.com https://maps.google.com https://www.google.com;

Der Unterschied zu YouTube: Google Maps hat keinen „Privacy-Enhanced Mode” wie youtube-nocookie.com. Das Einzige, was wir tun können, ist den Embed erst nach ausdrücklicher Zustimmung zu laden.

Demo

Naka Weekend Market in Phuket:

Naka Weekend Market, Phuket

Fazit

Gleiche Architektur wie der YouTube-Embed, drei wichtige Unterschiede:

  1. Kein Privacy-Modusyoutube-nocookie.com gibt es bei Maps nicht. Der Consent-Gate ist die einzige Schutzmaßnahme vor dem Seitenaufruf.
  2. App-Links immer sichtbar — Google Maps und Apple Maps als Fallback, auch ohne den eingebetteten iframe.
  3. Koordinaten-firstlat/lng Props bauen alles automatisch, kein manuelles Basteln von Embed-URLs.