Google Maps DSGVO-konform einbetten — Consent-Box mit App-Links
Eine leichtgewichtige Google-Maps-Embed-Komponente für Astro: kein Request vor Zustimmung, localStorage-Consent, automatische Apple Maps- und Google Maps-App-Links aus GPS-Koordinaten.
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:
- Vor dem Klick: null Requests an Google. Keine Karte, kein Tile, gar nichts.
- Echte Einwilligung — Klick auf „Einmalig anzeigen” oder „Zustimmen und merken” ist die Zustimmung (Art. 6 Abs. 1 lit. a DSGVO).
- 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
| Prop | Typ | Pflicht | Beschreibung |
|---|---|---|---|
lat | number | ✓ (oder src) | Breitengrad |
lng | number | ✓ (oder src) | Längengrad |
title | string | — | Ortsname (Beschriftung + App-Links) |
src | string | ✓ (oder lat+lng) | Manuell gebaute Embed-URL |
href | string | — | Überschreibt den Google-Maps-Link |
zoom | number | — | Zoom-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'stattstrict-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 vonyt-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:
Fazit
Gleiche Architektur wie der YouTube-Embed, drei wichtige Unterschiede:
- Kein Privacy-Modus —
youtube-nocookie.comgibt es bei Maps nicht. Der Consent-Gate ist die einzige Schutzmaßnahme vor dem Seitenaufruf. - App-Links immer sichtbar — Google Maps und Apple Maps als Fallback, auch ohne den eingebetteten iframe.
- Koordinaten-first —
lat/lngProps bauen alles automatisch, kein manuelles Basteln von Embed-URLs.