Embedding YouTube videos GDPR-compliant — three-tier consent box without a cookie
A lightweight YouTube embed component for Astro with real consent (once / remember / external), no cookie, and no third-party request before the click.
I wanted to embed a YouTube video in a post. The problem: the classic <iframe src="youtube.com/embed/…"> opens a pipeline to Google before the reader has clicked anything — cookies, fingerprint, IP, all without consent. A textbook GDPR violation.
What I needed:
- Before the click: zero requests to Google. No thumbnail, no script, nothing.
- Real consent — the click on “Load video” is the consent (Art. 6(1)(a) GDPR).
- Alternatives — “show once”, “agree and remember”, or simply “just the link”. No cookie, at most a
localStorageentry for the consent decision.
The result is a small Astro component implementing the pattern I cribbed from zeit.de in the wild.
The component
src/components/YouTubeEmbed.astro — core:
---
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>
Plus a small script that swaps the placeholder box for an <iframe> on youtube-nocookie.com when clicked:
<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>
Three important details:
youtube-nocookie.cominstead ofyoutube.com— the “privacy-enhanced mode”. Only sets data on playback, no personalization cookies.localStorageinstead of a cookie — the consent decision is stored locally in the browser only, never sent to a server. Under ePrivacy this counts as “strictly necessary” (it stores the consent itself, not tracking).- Auto-replace on
hasConsent— anyone who clicked “remember” sees the iframe directly on subsequent visits, but withoutautoplay(nobody likes videos that start playing unprompted).
i18n
Strings in src/i18n/ui.ts — per locale:
'youtube.externalContent': 'External content',
'youtube.description': 'This is where you\'ll find external content from youtube.com that complements this article.',
'youtube.serviceName': 'YouTube video player (youtube.com)',
'youtube.loadVideo': 'Show once',
'youtube.loadVideoRemember': 'Agree and remember',
'youtube.watchOnYouTube': 'Watch on YouTube',
'youtube.consentFooter': 'I agree to external content being displayed. This may result in personal data being transmitted to third-party providers. More information in our',
'youtube.privacyLink': 'privacy policy',
getLocaleFromUrl(Astro.url) reads the language from the path — done, works DE/EN automatically.
Adjusting the CSP
My strict Caddy CSP blocks every external iframe. The embed therefore needs a targeted exception:
- frame-ancestors 'none';
+ frame-src https://www.youtube-nocookie.com; frame-ancestors 'none';
img-src does not need to be extended because the placeholder doesn’t load any images from Google. No thumbnail, no Google request before consent — that’s the whole point of this construction.
Referrer-Policy: invisible snag
On my first test: Error 153 — Video player configuration error. Even on freely embeddable videos.
Cause: my site-wide Referrer-Policy: no-referrer. YouTube checks the referrer header on embed to verify which domain the iframe comes from. No referrer, no verification → generic Error 153.
Fix at the iframe level: referrerPolicy = 'strict-origin-when-cross-origin'. YouTube now only sees https://adrian-altner.de, no paths — acceptable privacy compromise, verification works.
Privacy policy
Extended with a dedicated section “Video embeds (YouTube)”: description of the three-tier solution, note on the localStorage entry yt-consent=1 (no cookie, local only, consent decision only), legal basis Art. 6(1)(a) GDPR, third-country transfer to the US via standard contractual clauses.
Demo
Here’s how it looks live — no request to Google before you press a button:
Open your browser’s network tab before clicking: you won’t see a single request to google.com, ytimg.com, or googlevideo.com. Only after the click does the iframe load — and even then only from youtube-nocookie.com.
Takeaway
No external consent library, no banner popup, no cookie. One component (~100 lines of Astro), one script block, two CSP lines in Caddy, one entry in the privacy policy. For a few occasional video embeds on a blog, the right order-of-magnitude compromise between “full consent infrastructure” and “just ignore it”.