Just another privacy-friendly Youtube shortcode

The direct integration of external resources is problematic for privacy reasons. I generally reject solutions with a cookie opt-in. I present here a solution to embed Youtube videos into a HUGO website that works without an external API and retrieves the automatically generated Youtube thumbnail and caches it locally.

@cmal has presented a solution with its own API (noapi-api). My solution is conceptually similar, keeping my API to the minimum and relatively easy to understand and customize.

The solution consists of two components:

  1. a php function that retrieves the thumbnail based on the Youtube ID and stores it locally (“API”).
  2. a shortcode that displays the thumbnail with a play button and links to Youtube.


First, it tries to retrieve the large thumbnail (maxresdefault.jpg) from Youtube. If that fails a smaller thumbnail is retrieved, which is actually always available. The image is stored locally in the folder “cache/”. If the image is already in the cache, it will not be retrieved again. At the end there is a redirect to this locally stored image. If there is no image at all (or in case of an error) a default image is displayed.

The code is relatively clear. I used curl to retrieve the data, because I need that for the two-step retrieval of the images and because it gives a reliable timeout. (I had bad experiences with file_get_contents in connection with timeout).

To prevent the script from being abused, I check the referer.

I assume that the script is located in the HUGO directory static/ and that there is a subdirectory cache/. This directory must be created manually. It is not created by the script. Depending on the provider and the type of upload, the cache directory may need to have more generous write permissions. This may need to be corrected after the upload. (I publish HUGO websites with a shell script via rsync and set write permissions in the directory public/cache/ after calling hugo and sync it afterwards with rsync -avz).

/* youtube_thumb.php: Fetches the Youtube thumbnail and saves it locally
   Usage: youtube_thumb.php?v=<Youtube-ID> Returns the cached image URL
   author: Erhard Maria Klein, https://www.weitblick.de

$whitelist = '/(yourdomain.com|localhost)/'; // Prevention of abuse
$defaultImage = 'images/default-image.jpg'; // Displayed when no image could be retrieved
$serverPath = './';
$v = '';
if (isset($_GET['v'])) $v = htmlspecialchars($_GET['v']);
$imageData = '';
$cacheFolder = 'cache/';
$fileName = $cacheFolder . $v . '.jpg';
$referer = $_SERVER['HTTP_REFERER'];

if (preg_match($whitelist, $referer, $match)) {
    // Check if the file already exists
    if (!file_exists($fileName)) {
        // retrieve maxresdefault.jpg
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1);
        curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)");
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
        curl_setopt($ch, CURLOPT_URL, 'https://img.youtube.com/vi/' . $v . '/maxresdefault.jpg');
        $imageData = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

        // If maxresdefault.jpg doesn't exist, retrieve 0.jpg
        if ($httpCode != 200) {
            curl_setopt($ch, CURLOPT_URL, 'https://img.youtube.com/vi/' . $v . '/0.jpg');
            $imageData = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            if ($httpCode != 200) $imageData = '';
        if (!empty($imageData)) {
            file_put_contents($serverPath . $fileName, $imageData);
            header("Location: /$fileName");
        } else {
            header("Location: /$defaultImage");
    } else {
        header("Location: /$fileName");

HUGO Shortcode youtube.html

{{- $src := .Get "src" | default (.Get 0) -}}
{{- $caption := .Get "caption" | default (.Get 1) | safeHTML -}}
<a href="https://youtu.be/{{ $src }}" target="_blank" title="externes Video (Link öffnet Youtube)" style="display: block; text-align: center; position: relative;">
		<svg width="100" height="100" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">
			<path fill="#ec322a" stroke="#fff" d="M15,4.1c1,0.1,2.3,0,3,0.8c0.8,0.8,0.9,2.1,0.9,3.1C19,9.2,19,10.9,19,12c-0.1,1.1,0,2.4-0.5,3.4c-0.5,1.1-1.4,1.5-2.5,1.6 c-1.2,0.1-8.6,0.1-11,0c-1.1-0.1-2.4-0.1-3.2-1c-0.7-0.8-0.7-2-0.8-3C1,11.8,1,10.1,1,8.9c0-1.1,0-2.4,0.5-3.4C2,4.5,3,4.3,4.1,4.2 C5.3,4.1,12.6,4,15,4.1z M8,7.5v6l5.5-3L8,7.5z"></path>
		<img src="/youtube_thumb.php?v={{ $src }}" alt="{{ $caption | default $.Page.Title }}" loading="lazy">
		{{ with $caption }}<figcaption style="text-align: left;">{{ . }} (Link öffnet Youtube)</figcaption>{{ end }}


Within the content youtube videos with {{< youtube ID "caption">}} are embedded - e.g. with
{{< youtube 0RKpf3rK57I "HUGO in 100 Seconds" >}}.

Alternatively, the parameters src and caption can be used:
{{< youtube src="0RKpf3rK57I" caption="HUGO in 100 seconds" >}}

Additional information

The php function is called in a normal <img> tag – for example.
<img src="/youtube_thumb.php?v=0RKpf3rK57I" >.
The script makes a redirect to the cached image. This therefore works well within static HTML code. The only dynamic element in this solution is the freestanding php script. This could even be installed on another server and act as an API for multiple sites (adjust $whitelist and the redirect url accordingly).

With each HUGO build, the cache directory is recreated – and without write permissions. So whoever uploads the site via FTP must remember to give write permissions to the folder in the public directory. I have integrated this into my deploy script (see above).

Also, the paths must be correct. My script assumes that the website is running in the root directory of a domain and uses a relative path specification for writing the image file to the cache. However, an absolute server path can also be entered in the $serverPath variable. The php script and the cache directory are located in the static/ folder and are transferred to public/ during the HUGO build.

The variable $whitelist contains a regular expression that must match the domain name. This prevents someone from misusing the script for their own purposes. So: don’t forget to customize it!

$defaultImage contains a path to a default image, which is displayed if no image could be fetched from Youtube – i.e. even if the Youtube ID was invalid.

The cached images are not automatically updated and do not expire. I kept the script simple on purpose. On the next HUGO build, the cache directory will be regenerated (empty) anyway. Otherwise the directory has to be deleted manually if needed.

(Translation of my german blog article “Datenschutzfreundliche Youtube-Thumbnails für HUGO”. There you can also see an example)