Hacker News new | past | comments | ask | show | jobs | submit login

Click2load is an improvement, but embeds still suck.

All I want is a plain link, so a while back I got fed up and wrote a short userscript to just rewrite the page. Works surprisingly well for how simple it is.

  // ==UserScript==
  // @name         Youtube UnEmbed
  // @version      0.1
  // @description  Converts embedded Youtube iframes into links
  // @match        *://*/*
  // @exclude      *://*.youtube.com/*
  // @exclude      *://*.reddit.com/*
  // @exclude      *://looptube.io/*
  // @grant        none
  // @run-at       document-idle
  // ==/UserScript==

  (function() {
  'use strict';

  const SITE = "https://www.youtube.com"; //m.youtube Invidious etc
  const LINK_TO_TIMESTAMP = true;
  const SHOW_PREVIEW_IMAGE = false;

  const replaceEmbeds = () => {
    document.querySelectorAll('iframe').forEach((frame) => {
      const frameURL = frame.src || frame.dataset?.src;
      if (!frameURL) return;
      const match = frameURL.match(/(^https?:)?\/\/(www\.)?youtube(-nocookie)?\.com\/embed\/([a-zA-Z0-9\-_]{11}).*?(\?.*((t|start)=([\dsmh]*)))?/i);
      if (match?.length == 9) {
        const newURL = SITE+"/watch?" + ((LINK_TO_TIMESTAMP && match[8]) ? "t="+match[8]+"&" : "") + "v="+match[4];
        const elem = document.createElement("a")
        elem.href = newURL;
        if (SHOW_PREVIEW_IMAGE) {
          let img = document.createElement("img");
          img.src = "https://i.ytimg.com/vi/"+match[4]+"/mqdefault.jpg";
          img.alt="Preview image of Youtube video";
          // 320 x 180 preview. For more resolution options see
          // https://medium.com/@viniciu_/how-to-get-the-default-thumbnail-url-for-a-youtube-video-b5497b3b6218
          elem.appendChild(img);
        } else {
          elem.innerHTML = newURL;
        }

        frame.outerHTML = elem.outerHTML;

        // common lazyload hiding methods
        elem.style.display = "block !important";
        elem.style.opacity = "100% !important";
        elem.style.background = "transparent !important";
        const parent = elem.parentElement;
        if (parent) {
          parent.style.display = "block !important";
          parent.style.opacity = "100% !important";
          parent.style.background = "transparent !important";
        }


      }
    });
  };

  replaceEmbeds();
  setInterval(replaceEmbeds, 3000);
  })();



Great script idea! keeping this. Note, this does not prevent the initial loading of the embed itself, just replaces the embed with the link. The total transfer size of a page is still the same. If only there was a way to prevent the loading AND keep the link replacement.


Thanks. Despite being Not A Project I did edit to add optional image previews and reuse the regex object, so grab the final version if you want.

I should have mentioned this is paired with uBlock Origin to block Youtube iframes (and indeed all iframes) globally. At the time I was writing it to unbreak embedded videos.

https://github.com/gorhill/uBlock/wiki/Dynamic-filtering:-qu...

https://github.com/gorhill/uBlock/wiki/Blocking-mode

https://github.com/gorhill/uBlock/wiki/Blocking-mode:-medium...

From uBO My Rules tab:

  * youtube-nocookie.com * block
  * youtube.com * block
  * ytimg.com * block
  youtube.com youtube.com * noop
  youtube.com ytimg.com * noop
To block all iframes (except sites you whitelist, see links above):

  * * 3p-frame block


Oh wow, I was just thinking that optional image previews would make this great, good stuff! The preview image doesn't auto-fit nicely in all layouts(using sonyalpharumors.com to test; works great for header banner embeds but potentially awkward sizing for post embeds) but I think that's due to the fixed image size and you not messing with the DOM outside of adding the img to it. Will you post this somewhere like github or greasyfork?

I do have the 3rd party iframes blocked globally in uBo Firefox and works great with your script to replace the placeholder elements but Safari lacks an alternative sadly afaik


Yeah I should put this somewhere! In the meantime, right after img.alt =... you can add

  // improve styling
  img.className = frame.className;
  img.id = frame.id;
  img.style.width = frame.width +"px";
That last line is a hack (eg resizing), but it's required to make the test website work since annoyingly their CSS uses :is(frame).


I suggest using MutationObserver instead of just running the replaceEmbeds function every 3s.


Intentionally left as an exercise for the reader. ;-D If you do, share it back! I'll switch to your version.

Remember I threw this together in about half an hour, and maybe that amount of cleanup to post here. "Works for me" is the order of the day. The extra debugging alone would be longer than the whole project!

Besides, the function that runs is so light it doesn't really seem worth it. It could even make performance worse, since for reliability you need to observe mutations across the entire DOM, which could occur a lot more often. So for performance you want to add some debouncing too, adding yet more complexity to what's supposed to be a 'quick and dirty' fix.


here it is. i am however showing the preview image and replacing it with the iframe on click. basically click2load. by using mutationobserver it seems i don't need ublock to block the iframe, because as soon as the browser tries to include it on the page it gets replaced anyway

(function() { 'use strict';

  const SITE = "https://www.youtube.com";
  const LINK_TO_TIMESTAMP = true;
  const SHOW_PREVIEW_IMAGE = true;

  const createPreviewElement = (videoId, newURL, frame) => {
    const container = document.createElement("div");
    container.setAttribute('data-yt-preview', 'true');
    container.style.position = "relative";
    container.style.width = frame.width + "px";
    container.style.height = frame.height + "px";
    container.style.cursor = "pointer";

    const img = document.createElement("img");
    img.src = `https://i.ytimg.com/vi/${videoId}/mqdefault.jpg`;
    img.alt = "Preview image of Youtube video";
    img.style.width = "100%";
    img.style.height = "100%";
    img.style.objectFit = "cover";

    const playButton = document.createElement("div");
    playButton.innerHTML = "▶";
    playButton.style.position = "absolute";
    playButton.style.top = "50%";
    playButton.style.left = "50%";
    playButton.style.transform = "translate(-50%, -50%)";
    playButton.style.fontSize = "48px";
    playButton.style.color = "white";
    playButton.style.backgroundColor = "rgba(0, 0, 0, 0.7)";
    playButton.style.borderRadius = "50%";
    playButton.style.width = "80px";
    playButton.style.height = "80px";
    playButton.style.display = "flex";
    playButton.style.justifyContent = "center";
    playButton.style.alignItems = "center";

    container.appendChild(img);
    container.appendChild(playButton);

    container.addEventListener("click", () => {
      const iframe = document.createElement("iframe");
      iframe.src = frame.src + (frame.src.includes('?') ? '&' : '?') + 'autoplay=1';
      iframe.width = frame.width;
      iframe.height = frame.height;
      iframe.frameBorder = "0";
      iframe.allow = "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture";
      iframe.allowFullscreen = true;
      iframe.setAttribute('data-yt-processed', 'true');
      container.parentNode.replaceChild(iframe, container);
    });

    return container;
  };

  const replaceEmbed = (frame) => {
    const frameURL = frame.src || frame.dataset?.src;
    if (!frameURL) return;
    const match = frameURL.match(/(^https?:)?\/\/(www\.)?youtube(-nocookie)?\.com\/embed\/([a-zA-Z0-9\-_]{11}).*?(\?.*((t|start)=([\dsmh]*)))?/i);
    if (match?.length == 9) {
      const videoId = match[4];
      const newURL = SITE + "/watch?" + ((LINK_TO_TIMESTAMP && match[8]) ? "t=" + match[8] + "&" : "") + "v=" + videoId;

      const previewElement = createPreviewElement(videoId, newURL, frame);
      frame.parentNode.replaceChild(previewElement, frame);

      // common lazyload hiding methods
      previewElement.style.display = "block !important";
      previewElement.style.opacity = "100% !important";
      previewElement.style.background = "transparent !important";
      const parent = previewElement.parentElement;
      if (parent) {
        parent.style.display = "block !important";
        parent.style.opacity = "100% !important";
        parent.style.background = "transparent !important";
      }
    }
  };

  const observeDOM = () => {
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        if (mutation.type === 'childList') {
          mutation.addedNodes.forEach((node) => {
            if (node.nodeType === Node.ELEMENT_NODE) {
              if (node.tagName === 'IFRAME' && !node.hasAttribute('data-yt-processed')) {
                replaceEmbed(node);
              } else {
                node.querySelectorAll('iframe:not([data-yt-processed])').forEach(replaceEmbed);
              }
            }
          });
        }
      });
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  };

  // Initial replacement
  document.querySelectorAll('iframe:not([data-yt-processed])').forEach(replaceEmbed);

  // Start observing DOM changes
  observeDOM();
})();


For sonyalpharumors.com, the image preview extends into the side panel for post embeds, do you have a fix?

Edit: On line 21 & 22, I changed the container width to grab `frame.parentNode.width` instead and did the same for height. Seems to work better for that site at least.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: