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.1 (latest)
- Activity
- New: be the first to try it
- Installs
- 0 installs
- Likes
- 0 likes
- Clones
- 0 clones
- 1// Playwright test: no request to YouTube or Google before the visitor presses Play.
- 2// Set PAGE to a page of yours that renders one embed. Runs offline: requests to
- 3// the player are answered locally, so the test never reaches YouTube.
- 5import { expect, test, type Request } from "@playwright/test";
- 7const PAGE = "/video-example";
- 9// Hosts that belong to the video provider or its parent company.
- 10const PROVIDER =
- 11 /(^|\.)(youtube|youtube-nocookie|ytimg|googlevideo|ggpht|google|gstatic|doubleclick|googleapis)\.(com|net)$/;
- 13function providerRequests(requests: Request[]): string[] {
- 14 return requests.map((r) => r.url()).filter((url) => PROVIDER.test(new URL(url).hostname));
- 15}
- 17test.beforeEach(async ({ context }) => {
- 18 // Start every test as a new visitor with no consent stored.
- 19 await context.clearCookies();
- 20 await context.route(
- 21 (url) => PROVIDER.test(url.hostname),
- 22 (route) =>
- 23 route.fulfill({
- 24 status: 200,
- 25 contentType: "text/html",
- 26 body: "<!doctype html><title>player</title>",
- 27 }),
- 28 );
- 29});
- 31test("nothing loads from YouTube before the click", async ({ page }) => {
- 32 const requests: Request[] = [];
- 33 page.on("request", (r) => requests.push(r));
- 35 await page.goto(PAGE);
- 36 await page.waitForLoadState("networkidle");
- 37 // Scroll the placeholder into view too, so lazy loading cannot hide a request.
- 38 await page.getByRole("button", { name: /^Play video/ }).scrollIntoViewIfNeeded();
- 39 await page.waitForLoadState("networkidle");
- 41 expect(providerRequests(requests)).toEqual([]);
- 42 await expect(page.locator("iframe")).toHaveCount(0);
- 43});
- 45test("pressing Play with the keyboard loads the privacy-enhanced player and moves focus", async ({
- 46 page,
- 47}) => {
- 48 await page.goto(PAGE);
- 49 const play = page.getByRole("button", { name: /^Play video/ });
- 50 await play.focus();
- 51 await page.keyboard.press("Enter");
- 53 const frame = page.locator("iframe");
- 54 await expect(frame).toHaveCount(1);
- 55 const src = new URL((await frame.getAttribute("src")) ?? "");
- 56 expect(src.hostname).toBe("www.youtube-nocookie.com");
- 57 expect(src.searchParams.get("autoplay")).toBe("1");
- 58 await expect(frame).toHaveAttribute("title", /.+/);
- 59 await expect(frame).toBeFocused();
- 60});
- 62test("the box keeps its size when the player replaces the placeholder", async ({ page }) => {
- 63 await page.goto(PAGE);
- 64 const play = page.getByRole("button", { name: /^Play video/ });
- 65 // The 16:9 box: [data-yt-box] in the React template, .yt-embed in the plain one.
- 66 const box = page.locator("[data-yt-box], .yt-embed").first();
- 67 const before = await box.boundingBox();
- 68 await play.click();
- 69 await expect(page.locator("iframe")).toHaveCount(1);
- 70 const after = await box.boundingBox();
- 71 expect(before && after).toBeTruthy();
- 72 expect(Math.abs((before?.height ?? 0) - (after?.height ?? 0))).toBeLessThan(2);
- 73});
