YouTube-Videos DSGVO-konform einbetten — dreistufige Consent-Box ohne Cookie


Ich wollte in einem Post ein YouTube-Video einbetten. Problem: der klassische <iframe src="youtube.com/embed/…"> öffnet eine Pipeline zu Google, noch bevor der Leser irgendwas geklickt hat — Cookies, Fingerprint, IP, und das alles ohne Einwilligung. DSGVO-technisch ein Lehrbuchverstoß.

Was ich gebraucht habe:

  1. Vor dem Klick: null Requests an Google. Kein Thumbnail, kein Script, gar nichts.
  2. Echte Einwilligung — der Klick auf „Video laden” ist die Zustimmung (Art. 6 Abs. 1 lit. a DSGVO).
  3. Alternativen — „einmalig anzeigen”, „zustimmen und merken”, oder einfach „nur den Link”. Kein Cookie, maximal ein localStorage-Eintrag für die Einwilligungsentscheidung.

Rausgekommen ist eine kleine Astro-Komponente, die das Muster umsetzt, das ich in der Praxis bei zeit.de abgeguckt habe.

Die Komponente

src/components/YouTubeEmbed.astro — Kern:

---
import { getLocaleFromUrl, t } from '~/i18n/ui';
interface Props { id: string; title?: string }
const { id, title } = Astro.props;
const locale = getLocaleFromUrl(Astro.url);
const privacyHref = locale === 'de' ? '/datenschutz/' : '/en/privacy-policy/';
const youtubeUrl = `https://www.youtube.com/watch?v=${id}`;
---

<figure class="youtube-embed">
  <div class="yt-consent" data-id={id} data-title={title ?? ''}>
    <h3>{t(locale, 'youtube.externalContent')}</h3>
    <p>{t(locale, 'youtube.description')}</p>
    <p>
      {t(locale, 'youtube.serviceName')}
      {title && <><br /><strong>{title}</strong></>}
    </p>
    <div class="yt-consent-buttons">
      <button type="button" data-remember="true">{t(locale, 'youtube.loadVideoRemember')}</button>
      <button type="button">{t(locale, 'youtube.loadVideo')}</button>
      <a href={youtubeUrl} target="_blank" rel="noopener noreferrer">{t(locale, 'youtube.watchOnYouTube')}</a>
    </div>
    <p class="footer">
      {t(locale, 'youtube.consentFooter')}
      <a href={privacyHref}>{t(locale, 'youtube.privacyLink')}</a>.
    </p>
  </div>
</figure>

Dazu ein kleines Script, das die Platzhalter-Box beim Klick gegen einen <iframe> auf youtube-nocookie.com tauscht:

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

  function buildIframe(id, title, autoplay) {
    const iframe = document.createElement('iframe');
    iframe.src = `https://www.youtube-nocookie.com/embed/${id}?rel=0${autoplay ? '&autoplay=1' : ''}`;
    iframe.allow = 'autoplay; encrypted-media; picture-in-picture; fullscreen';
    iframe.allowFullscreen = true;
    iframe.loading = 'lazy';
    iframe.referrerPolicy = 'strict-origin-when-cross-origin';
    if (title) iframe.title = title;
    return iframe;
  }

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

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

Drei wichtige Details:

  • youtube-nocookie.com statt youtube.com — der „erweiterte Datenschutzmodus”. Setzt erst bei der Wiedergabe Daten, keine Personalisierungs-Cookies.
  • localStorage statt Cookie — die Einwilligungsentscheidung wird nur lokal im Browser gespeichert und nie an einen Server geschickt. Zählt nach ePrivacy als „unbedingt erforderlich” (es speichert die Einwilligung selbst, kein Tracking).
  • Auto-Replace bei hasConsent — wer „merken” geklickt hat, sieht bei Folgebesuchen direkt den iframe, aber ohne autoplay (niemand mag Videos, die ungefragt losspielen).

i18n

Strings in src/i18n/ui.ts — je Locale:

'youtube.externalContent': 'Externer Inhalt',
'youtube.description': 'An dieser Stelle findest du einen externen Inhalt von youtube.com, der den Artikel ergänzt.',
'youtube.serviceName': 'YouTube Video Player (youtube.com)',
'youtube.loadVideo': 'Einmalig anzeigen',
'youtube.loadVideoRemember': 'Zustimmen und merken',
'youtube.watchOnYouTube': 'Auf YouTube ansehen',
'youtube.consentFooter': 'Ich bin damit einverstanden, dass mir externe Inhalte angezeigt werden. Damit können personenbezogene Daten an Drittanbieter übermittelt werden. Mehr dazu in der',
'youtube.privacyLink': 'Datenschutzerklärung',

getLocaleFromUrl(Astro.url) liest die Sprache aus dem Pfad — fertig, läuft automatisch DE/EN korrekt.

CSP anpassen

Meine strikte Caddy-CSP blockt jeden externen iframe. Der Embed braucht deswegen eine gezielte Freigabe:

- frame-ancestors 'none';
+ frame-src https://www.youtube-nocookie.com; frame-ancestors 'none';

img-src muss nicht erweitert werden, weil der Platzhalter keine Bilder von Google lädt. Kein Thumbnail, kein Google-Request vor Consent — das ist der Witz an der ganzen Konstruktion.

Referrer-Policy: unsichtbarer Stolperstein

Beim ersten Test: Error 153 — Fehler bei der Konfiguration des Videoplayers. Auch bei absolut einbettbaren Videos.

Ursache: meine seitenweite Referrer-Policy: no-referrer. YouTube prüft beim Embed per Referrer-Header, von welcher Domain der iframe kommt. Ohne Referrer keine Verifikation → generischer Error 153.

Fix auf iframe-Ebene: referrerPolicy = 'strict-origin-when-cross-origin'. YouTube sieht jetzt nur noch https://adrian-altner.de, keine Pfade — Privacy-Kompromiss vertretbar, Verifikation funktioniert.

Datenschutzerklärung

Ergänzt um einen eigenen Abschnitt „Video-Einbettungen (YouTube)”: Beschreibung der dreistufigen Lösung, Hinweis auf den localStorage-Eintrag yt-consent=1 (kein Cookie, nur lokal, nur Einwilligungsentscheidung), Rechtsgrundlage Art. 6 Abs. 1 lit. a DSGVO, Drittlandtransfer USA über Standardvertragsklauseln.

Demo

So sieht’s live aus — kein Request an Google, bevor du einen Button drückst:

Naka Weekend Market, Phuket

Öffne die Netzwerk-Tab deines Browsers vor dem Klick: Du wirst keinen einzigen Request zu google.com, ytimg.com oder googlevideo.com sehen. Erst nach dem Klick lädt der iframe — und auch dann nur auf youtube-nocookie.com.

Fazit

Keine externe Consent-Library, kein Banner-Popup, kein Cookie. Eine Komponente (~100 Zeilen Astro), ein Script-Block, zwei CSP-Zeilen in Caddy, ein Eintrag in der Datenschutzerklärung. Für ein paar gelegentliche Video-Embeds auf einem Blog der richtige Größenordnungs-Kompromiss zwischen „vollständige Einwilligungsinfrastruktur” und „einfach ignorieren”.