Embed YouTube videos without tracking cookies
@slashclone/privacy-friendly-youtube-embed
Embed YouTube videos on your site without loading anything from Google until the visitor presses Play or allows videos. A click-to-play placeholder with a local poster swaps in the youtube-nocookie.com player, with a keyboard-friendly button, no layout shift, CSP entries, and a Playwright test that proves no YouTube request happens first. React and HTML templates. Not legal advice.
SkillDevOpsv0.1.0
- Activity
- New: be the first to try it
- Installs
- 0 installs
- Likes
- 0 likes
- Clones
- 0 clones
- 1// Click-to-play YouTube embeds for plain HTML. No dependencies, no build step.
- 2//
- 3// Markup (see youtube-embed.html):
- 4// <div class="yt-embed" data-video="VIDEO_ID_OR_LINK" data-title="Title"
- 5// data-duration="4:12" data-poster="/posters/intro.jpg"></div>
- 6//
- 7// Nothing is fetched from Google until the visitor presses Play, or until your
- 8// consent layer calls YouTubeEmbeds.setAllowed(true) because the visitor allowed
- 9// embedded videos. To offer "always allow videos" here, set
- 10// YouTubeEmbeds.onAllowAll = function () { ... } before calling init().
- 12(function () {
- 13 "use strict";
- 15 var PLAYER_ORIGIN = "https://www.youtube-nocookie.com";
- 16 var ID = /^[A-Za-z0-9_-]{11}$/;
- 17 var allowed = false;
- 19 function parseVideoId(input) {
- 20 if (ID.test(input)) return input;
- 21 var u;
- 22 try {
- 23 u = new URL(input);
- 24 } catch (e) {
- 25 return null;
- 26 }
- 27 if (u.protocol !== "https:" && u.protocol !== "http:") return null;
- 28 var host = u.hostname.toLowerCase().replace(/^(www|m|music)\./, "");
- 29 var id = null;
- 30 if (host === "youtu.be") id = u.pathname.split("/")[1] || null;
- 31 else if (host === "youtube.com" || host === "youtube-nocookie.com") {
- 32 if (u.pathname === "/watch") id = u.searchParams.get("v");
- 33 else {
- 34 var m = /^\/(?:embed|shorts|live|v)\/([^/?#]+)/.exec(u.pathname);
- 35 id = m ? m[1] : null;
- 36 }
- 37 }
- 38 return id && ID.test(id) ? id : null;
- 39 }
- 41 function el(tag, attrs, text) {
- 42 var n = document.createElement(tag);
- 43 for (var k in attrs) n.setAttribute(k, attrs[k]);
- 44 if (text) n.textContent = text;
- 45 return n;
- 46 }
- 48 function playerSrc(id, box, autoplay) {
- 49 var p = new URLSearchParams({ rel: "0", playsinline: "1" });
- 50 if (autoplay) p.set("autoplay", "1");
- 51 var start = parseInt(box.dataset.start || "", 10);
- 52 if (start > 0) p.set("start", String(start));
- 53 if (box.dataset.captions === "true") p.set("cc_load_policy", "1");
- 54 return PLAYER_ORIGIN + "/embed/" + id + "?" + p.toString();
- 55 }
- 57 function showPlayer(box, clicked) {
- 58 if (box.dataset.state === "player") return;
- 59 var id = box.dataset.videoId;
- 60 var frame = el("iframe", {
- 61 src: playerSrc(id, box, clicked),
- 62 title: box.dataset.title || "YouTube video",
- 63 loading: clicked ? "eager" : "lazy",
- 64 allow: "autoplay; encrypted-media; picture-in-picture; fullscreen",
- 65 allowfullscreen: "",
- 66 referrerpolicy: "strict-origin-when-cross-origin",
- 67 });
- 68 frame.className = "yt-embed__fill";
- 69 box.replaceChildren(frame);
- 70 box.dataset.state = "player";
- 71 if (clicked) frame.focus();
- 72 }
- 74 function showPlaceholder(box) {
- 75 var id = box.dataset.videoId;
- 76 var title = box.dataset.title || "YouTube video";
- 77 box.replaceChildren();
- 78 box.dataset.state = "placeholder";
- 80 if (box.dataset.poster) {
- 81 var img = el("img", { src: box.dataset.poster, alt: "", loading: "lazy", decoding: "async" });
- 82 img.className = "yt-embed__fill yt-embed__poster";
- 83 box.appendChild(img);
- 84 }
- 86 var layer = el("div", {});
- 87 layer.className = "yt-embed__fill yt-embed__layer";
- 89 var heading = el("p", {}, title);
- 90 heading.className = "yt-embed__title";
- 91 if (box.dataset.duration)
- 92 heading.appendChild(el("span", {}, " (" + box.dataset.duration + ")"));
- 94 var play = el("button", { type: "button", "aria-label": "Play video: " + title }, "Play video");
- 95 play.className = "yt-embed__play";
- 96 play.addEventListener("click", function () {
- 97 showPlayer(box, true);
- 98 });
- 100 var note = el("p", {}, "Playing loads the video from YouTube, which may set cookies. ");
- 101 note.className = "yt-embed__note";
- 102 note.appendChild(
- 103 el(
- 104 "a",
- 105 {
- 106 href: "https://www.youtube.com/watch?v=" + id,
- 107 rel: "noopener noreferrer",
- 108 target: "_blank",
- 109 },
- 110 "Watch on YouTube",
- 111 ),
- 112 );
- 113 if (typeof api.onAllowAll === "function") {
- 114 note.appendChild(document.createTextNode(" or "));
- 115 var always = el("button", { type: "button" }, "always allow videos");
- 116 always.className = "yt-embed__link";
- 117 always.addEventListener("click", function () {
- 118 api.onAllowAll();
- 119 });
- 120 note.appendChild(always);
- 121 }
- 122 note.appendChild(document.createTextNode("."));
- 124 layer.appendChild(heading);
- 125 layer.appendChild(play);
- 126 layer.appendChild(note);
- 127 box.appendChild(layer);
- 128 }
- 130 function init(root) {
- 131 var boxes = (root || document).querySelectorAll(".yt-embed:not([data-state])");
- 132 for (var i = 0; i < boxes.length; i++) {
- 133 var box = boxes[i];
- 134 var id = parseVideoId(box.dataset.video || "");
- 135 if (!id) {
- 136 box.hidden = true;
- 137 continue;
- 138 }
- 139 box.dataset.videoId = id;
- 140 if (allowed) showPlayer(box, false);
- 141 else showPlaceholder(box);
- 142 }
- 143 }
- 145 // Call with true when the visitor grants embedded videos, false when they revoke.
- 146 // Revoking replaces players with placeholders; cookies YouTube already set are
- 147 // the consent layer's job to explain or clear.
- 148 function setAllowed(value) {
- 149 allowed = !!value;
- 150 var boxes = document.querySelectorAll(".yt-embed[data-video-id]");
- 151 for (var i = 0; i < boxes.length; i++) {
- 152 if (allowed) showPlayer(boxes[i], false);
- 153 else showPlaceholder(boxes[i]);
- 154 }
- 155 }
- 157 var api = { init: init, setAllowed: setAllowed, parseVideoId: parseVideoId, onAllowAll: null };
- 158 window.YouTubeEmbeds = api;
- 159})();
