Privacy-friendly Youtube shortcode

Something that bugs me on the web is the way 3rd party trackers find their way around most websites, to creepily gather data about everyone. Static-site generation could pave the way for saner websites, however most people just use them to plant some Javascript snitch on everyone’s computer.

That’s precisely what we do when we call a Youtube/Twitter shortcode. We allow these evil multi-billion dollars corporations to spy on each and every one of our users. Because our mission as computer people is to enhance everyone’s freedom and privacy, let me introduce to you today my first attempt at a privacy-friendly Youtube shortcode.

How to set it up

You need a script to fetch the image associated with the Youtube video, generate a thumbnail and send it in base64 alongside its dimensions. I’m currently using a PHP script I wrote for this (very hacky work-in-progress), but you may use any sort of script to do that.

Then, my template generates an autonomous responsive video preview that does not contact Youtube on the client-side:

{{ $api := "http://noapi.lan" }}

{{ if eq (len .Params) 2 }}{{ .Scratch.Set "width" (.Get 1) }}{{ else }}{{ .Scratch.Set "width" "512" }}{{ end }}
{{ if eq (len .Params) 3 }}{{ .Scratch.Set "quality" (.Get 2) }}{{ else }}{{ .Scratch.Set "quality" "30" }}{{ end }}
{{ $width := .Scratch.Get "width" }}
{{ $quality := .Scratch.Get "quality" }}
{{ $id := .Get 0 }}
{{ $request := printf "%s/youtube/preview/%s/%s/%s" $api $id $width $quality }}
{{ $result := getJSON $request }}
{{ if eq $result.status "ok" }}
    <div style="overflow: hidden;">
        <style scoped="scoped">
        	div { background: url('data:image/jpeg;base64,{{ $result.content.image64 | safeCSS }}');
                  background-size: cover; background-repeat: no-repeat; text-align: center; box-shadow: inset 0 0 10px #000000; margin: 1rem auto;
                  width: 100%; max-height: {{ $result.content.previewDim.h }}px;
            }
            @media screen and (min-width: {{ $result.content.previewDim.w | safeCSS }}px) {
                div { max-width: {{ $result.content.previewDim.w | safeCSS }}px; height: {{ $result.content.previewDim.h | safeCSS }}px; }
                img { position: relative; top: 50%; transform: translateY(-50%); margin: auto auto; }
            }
        </style>
        <a href="https://youtube.com/watch?v={{ $id }}"><img alt="Play video in a new tab" src="{{ "/images/widgets/play-btn.png" | relURL }}"></a>
    </div>
{{ end }}

How to use it

Basic usage is backwards-compatible with the internal youtube shortcode. Like so:

{{< youtube "0hIN3yS3owY" >}}

The shortcode also takes a second and third optional parameters for the thumbnail width, and the quality setting passed to ImageMagick for thumbnail generation.

So you may use it like that as well:

{{< youtube "uGo6z7oxTBg" 512 60 >}}

The future

For now my backend API’s interface is far from being standard, but if some more people are interested in developments like this, we could agree on following some standards such as ActivityStreams 2.0 (the “social” JSON standard) to describe the data, so that multiple backends can be switched transparently.

5 Likes

As a visitor to your site I would be very annoyed about new popups just to play a youtube video.

Privacy-friendly-er would be to use https://www.youtube-nocookie.com/embed/YOUTUBE-VIDEO-ID as video url and then letting the video play in the normal page. Also the youtube api has several parameters to set things like “related videos” or branding to less intrusive values.

I am using a solution like this one:

And thumbnails are loaded via shell script once, not every time on build:

#!/bin/bash

# retrieve thumbnail
curl -o static/images/youtube/$1.jpg https://i.ytimg.com/vi/$1/hqdefault.jpg

Call it like scriptname.sh YOUTUBEID.

In the javascript then I am adding some parameters:

    var embed = "https://www.youtube.com/embed/ID?autoplay=1";
    embed += '&controls=0'; //show video player controls
    embed += '&hl=de'; // set language to de
    embed += '&iv_load_policy=3'; // do not show video annotations by default
    embed += '&modestbranding=1'; // not too much branding
    embed += '&rel=0'; // do not show related videos
    embed += '&showinfo=0'; // do not show information

I did not test it with https://www.youtube-nocookie.com/embed/ though, but it should work the same.

1 Like

That’s really interesting, an elegant solution! Can I dare to ask though, if you have a script to import the video thumbnail, why do you load it from the JS? Is it to reduce initial loading time?
(Have you considered reducing image size from your bash script instead/furthermore?)

It’s not exactly a popup as i’m not running any client-side code. It’s a simple link that will follow your browser’s preferences. I personally like it on a website with a video when i can just middle-click to open it in a new tab and continue reading :slight_smile:

Embedding youtube-nocookie directly is not more privacy-friendly. They don’t get the cookie, but they still have all the access logs and other Javascript-based ways of identifying people. My goal is that only people who explicitely consent (by clicking) should be trackable by Google.

That’s nice! Have you considered plugging this script to your build pipeline (with a git hook server-side) so you can fetch the image and push it to the repo before the site gets built? That would be more user-friendly. Maybe i’ll try to do this :slight_smile:

I mean of course using youtube-nocookie.com in your code, instead of youtube.com - one cookie less is always good :wink: it also (as far as I know) removes all the related items stuff from the video that comes up at the end.

I don’t have the youtube thumbnail loader in my build workflow… not sure how I would do this. But it would be an idea to do this somehow directly in my youtube shortcode. I’ll have a look later on.

How could you implement something like this from a shortcode? I don’t think there’s a download function available from the templates (except for JSON/CSV), and we don’t have an exec function to execute arbitrary scripts. See here, here, and here for more discussions on the matter.

If you have a workaround though, I would love to hear about it :slight_smile:

In the meantime, I’ve had some fun developing some sort of content plugin system for my build script. You can find it here. build.sh is the build script in question, but what’s more interesting to our use-case is pre-build.sh.

pre-build.sh will look for all scripts in hooks/pre-build/*.sh and run them a first time, expecting them to reply with a regex that interests them. If the content changes match the regex, the changes will be passed to the script (say, a youtube plugin) to do its stuff.

So here is the youtube.sh hook that fetches the image from Youtube directly when content with the youtube shortcode has been added/modified/deleted. The image is minified with imagemagick, and both versions are kept in static/external/youtube folder.

youtube.sh should really be called from pre-build.sh, because it supports different sources of content changes out-of-the-box:

  • pre-build.sh init will look for matches inside the whole content folder (good to fetch previews for youtube videos added with the internal youtube shortcode that are long since pushed)
  • pre-build.sh uncommitted will look for matches inside your edited but uncommitted content files
  • pre-build.sh committed will look for matches inside your committed but unpushed stuff, and auto-commit additional files (will not try to hide by amending your commit)

Personally i’m using the last two in my build file. Here’s my new youtube shortcode (way shorter and still responsive as fuck :laughing:).

TODO: Collect the garbage when a video is deleted from the whole content folder. Write more plugins like this? For webmention.io maybe? Twitter?

For completeness sake I just want to add that youtube-nocookie sets persistent Local storage that tracks users once the play button is pressed.

I really like this idea. I was looking for a way to do this: avoid loading any data from youtube or vimeo unless the user clicks on the video. All served files should come from one domain only, mine. And my goal is to make my site look nice and work without javascript :slight_smile:

To download thumbnails you can use

$ youtube-dl --skip-download --write-thumbnail "https://player.vimeo.com/video/169250722"

and it works for many video sites, not just youtube.
youtube-dl packages are available for various OSes.

Update: use -o for a file name like ID.jpg:

$ youtube-dl --skip-download -o "%(id)s." --write-thumbnail "https://player.vimeo.com/video/169250722"
$ ls
$ 169250722.jpg

This is what I did so far:

#!/bin/bash
# Script to download thumbnails from youtube, vimeo, etc
# Usage: thumb-dl.sh https://www.youtube.com/embed/23d0Q_WLNig
#        will produce 23d0Q_WLNig.jpg
youtube-dl --skip-download -o "%(id)s." --write-thumbnail "$1"
<!-- Vimeo shortcode. Usage: {{% vimeo 113695260 "1:21" %}} -->
<div class="videoPlayer">
  <img class="thumbnail" src="{{ index .Params 0 }}.jpg">
  <span class="time darkText">{{ index .Params 1 }}</span>
  <span class="where darkText">Watch in Vimeo</span>
  <a href="//player.vimeo.com/video/{{ index .Params 0 }}" class="playWrapper"></a>
</div>
<!-- YouTube shortcode. Usage: {{% youtube 23d0Q_WLNig "1:16" %}} -->
<div class="videoPlayer">
  <img class="thumbnail" src="{{ index .Params 0 }}.jpg">
  <span class="time darkText">{{ index .Params 1 }}</span>
  <span class="where darkText">Watch in YouTube</span>
  <a href="//www.youtube.com/embed/{{ index .Params 0 }}" class="playWrapper"></a>
</div>
  /* sass */
  .videoPlayer
    position: relative
    width: 100% /* adjustable */

    .thumbnail
      display: block
      width: 100%
      height: 100%

    .time
      right: 3px

    .where
      left: 3px

    .darkText
      position: absolute
      z-index: 2
      bottom: 3px
      padding: 2px 5px
      background-color: rgba(0, 0, 0, 0.6)
      color: white
      user-select: none
      cursor: pointer

    .playWrapper
      opacity: 0.8
      position: absolute
      z-index: 1
      top: 0
      width: 100%
      height: 100%
      background: url("/static/i/video-play-button.png") no-repeat scroll center center / 50px 50px

      .playBtn 
        position: absolute
        z-index: 2
        width: 70px
        height: 70px
        left: 0
        right: 0
        top: 0
        bottom: 0
        margin: auto

    .playWrapper:hover
      opacity: 1
      background: rgba(0, 0, 0, 0.5) url("/static/i/video-play-button.png") no-repeat scroll center center / 50px 50px

If people don’t mind using JavaScript, you could replace the image by an iframe of the same size with autoplay when clicking the player. That would be almost like embedding (except for some blinking while loading) and visitors would not load all the remote embed assets by default anymore.

3 Likes

Are you aware of the Privacy Enhanced Internal Shortcodes? See: https://gohugo.io/about/hugo-and-gdpr/

We do not have YouTube (due to the way that YouTube serves video thumbnails) but there is a vimeo_simple internal shortcode that can be activated through the Privacy Config and here is what it contains:

I wasn’t aware of them. Do I understand it right that this approach would download the remote JSON once when building the site? But I guess it’s still downloading files from a remote server (at least the thumbnail), right? Unless there’s a way to download it and host it locally.

The answer to your questions is yes. getJSON fetches the file from the remote server but it there will still be a remote call for the thumbnail.

And currently there is no way to download and host locally that thumb with Hugo only.