Oh, embed!
I wrote yesterday about how messing about on your own website can be a welcome distraction. I did some tinkering with adactio.com on the weekend that you might be interested in.
Let me set the scene…
I’ve started recording and publishing a tune a day. I grab my mandolin, open up Quicktime and make a movie of me playing a jig, a reel, or some other type of Irish tune. I include a link to that tune on The Session and a screenshot of the sheet music for anyone who wants to play along. And I embed the short movie clip that I’ve uploaded to YouTube.
Now it’s not the first time I’ve embedded YouTube videos into my site. But with the increased frequency of posting a tune a day, the front page of adactio.com ended up with multiple embeds. That is not good for performance—my Lighthouse score took quite a hit. Worst of all, if a visitor doesn’t end up playing an embedded video, all of the markup, CSS, and JavaScript in the embedded iframe
has been delivered for nothing.
Meanwhile over on The Session, I’ve got a strategy for embedding YouTube videos that’s better for performance. Whenever somebody posts a link to a video on YouTube, the thumbnail of the video is embedded. Only when you click the thumbnail does that image get swapped out for the iframe
with the video.
That’s what I needed to do here on adactio.com.
First off, I should explain how I’m embedding things generally ‘round here. Whenever I post a link or a note that has a URL in it, I run that URL through a little PHP script called getEmbedCode.php
.
That code checks to see if the URL is from a service that provides an oEmbed endpoint. A what-Embed? oEmbed!
oEmbed is like a minimum viable read-only API. It was specced out by Leah and friends years back. You ping a URL like this:
http://example.com/oembed?url=https://example.com/thing
In this case http://example.com/oembed
is the endpoint and url
is the value of a URL from that provider. Here’s a real life example from YouTube:
https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=-eiqhVmSPcs
So https://www.youtube.com/oembed
is the endpoint and url
is the address of any video on YouTube.
You get back some JSON with a pre-defined list of values like title
and html
. That html
payload is the markup for your embed code.
By default, YouTube sends back markup like this:
<iframe
width="480"
height="270"
src="https://www.youtube.com/embed/-eiqhVmSPcs?feature=oembed"
frameborder="0
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>
But now I want to use an img
instead of an iframe
. One of the other values returned is thumbnail_url
. That’s the URL of a thumbnail image that looks something like this:
https://i.ytimg.com/vi/-eiqhVmSPcs/hqdefault.jpg
In fact, once you know the ID of a YouTube video (the ?v=
bit in a YouTube URL), you can figure out the path to multiple images of different sizes:
- 120 × 90:
https://i.ytimg.com/vi/-eiqhVmSPcs/default.jpg
- 320 × 180:
https://i.ytimg.com/vi/-eiqhVmSPcs/mqdefault.jpg
- 480 × 360:
https://i.ytimg.com/vi/-eiqhVmSPcs/hqdefault.jpg
- 1280 × 720:
https://i.ytimg.com/vi/-eiqhVmSPcs/maxresdefault.jpg
(Although that last one—maxresdefault.jpg
—might not work for older videos.)
Okay, so I need to extract the ID from the YouTube URL. Here’s the PHP I use to do that:
parse_str(parse_url($url, PHP_URL_QUERY), $arguments);
$id = $arguments['v'];
Then I can put together some HTML like this:
<div>
<a class="videoimglink" href="'.$url.'">
<img width="100%" loading="lazy"
src="https://i.ytimg.com/vi/'.$id.'/default.jpg"
alt="'.$response['title'].'"
srcset="
https://i.ytimg.com/vi/'.$id.'/mqdefault.jpg 320w,
https://i.ytimg.com/vi/'.$id.'/hqdefault.jpg 480w,
https://i.ytimg.com/vi/'.$id.'/maxresdefault.jpg 1280w
">
</a>
</div>
Now I’ve got a clickable responsive image that links through to the video on YouTube. Time to enhance. I’m going to add a smidgen of JavaScript to listen for a click on that link.
Over on The Session, I’m using addEventListener
but here on adactio.com I’m going to be dirty and listen for the event directly in the markup using the onclick
attribute.
When the link is clicked, I nuke the link and the image using innerHTML
. This injects an iframe where the link used to be (by updating the innerHTML
value of the link’s parentNode
).
onclick="event.preventDefault();
this.parentNode.innerHTML='<iframe src=https://www.youtube-nocookie.com/embed/'.$id.'?autoplay=1></iframe>'"
But notice that I’m not using the default YouTube URL for the iframe. That would be:
https://www.youtube.com/embed/-eiqhVmSPcs
Instead I’m swapping out the domain youtube.com
for youtube-nocookie.com
:
https://www.youtube-nocookie.com/embed/-eiqhVmSPcs
I can’t remember where I first came across this undocumented parallel version of YouTube that has, yes, you guessed it, no cookies. It turns out that, not only is the default YouTube embed code bad for performance, it is—unsurprisingly—bad for privacy too. So the youtube-nocookie.com
domain can protect your site’s visitors from intrusive tracking. Pass it on.
Anyway, I’ve got the markup I want now:
<div>
<a class="videoimglink" href="https://www.youtube.com/watch?v=-eiqhVmSPcs"
onclick="event.preventDefault();
this.parentNode.innerHTML='<iframe src=https://www.youtube-nocookie.com/embed/-eiqhVmSPcs?autoplay=1></iframe>'">
<img width="100%" loading="lazy"
src="https://i.ytimg.com/vi/-eiqhVmSPcs/default.jpg"
alt="The Banks Of Lough Gowna (jig) on mandolin"
srcset="
https://i.ytimg.com/vi/-eiqhVmSPcs/mqdefault.jpg 320w,
https://i.ytimg.com/vi/-eiqhVmSPcs/hqdefault.jpg 480w,
https://i.ytimg.com/vi/-eiqhVmSPcs/maxresdefault.jpg 1280w
">
</a>
</div>
The functionality is all there. But I want to style the embedded images to look more like playable videos. Time to break out some CSS (this is why I added the videoimglink
class to the YouTube link).
.videoimglink {
display: block;
position: relative;
}
I’m going to use generated content to create a play button icon. Because I can’t use generated content on an img
element, I’m applying these styles to the containing .videoimglink
a
element.
.videoimglink::before {
content: '▶';
}
I was going to make an SVG but then I realised I could just be lazy and use the unicode character instead.
Right. Time to draw the rest of the fucking owl:
.videoimglink::before {
content: '▶';
display: inline-block;
position: absolute;
background-color: var(--background-color);
color: var(--link-color);
border-radius: 50%;
width: 10vmax;
height: 10vmax;
top: calc(50% - 5vmax);
left: calc(50% - 5vmax);
font-size: 6vmax;
text-align: center;
text-indent: 1vmax;
opacity: 0.5;
}
That’s a bunch of instructions for sizing and positioning. I’d explain it, but that would require me to understand it and frankly, I’m not entirely sure I do. But it works. I think.
With a translucent play icon positioned over the thumbnail, all that’s left is to add a :hover
style to adjust the opacity:
.videoimglink:hover::before,
.videoimglink:focus::before {
opacity: 0.75;
}
Wheresoever thou useth :hover
, thou shalt also useth :focus
.
Okay. It’s good enough. Ship it!
If you embed YouTube videos on your site, and you’d like to make them more performant, check out this custom element that Paul made: Lite YouTube Embed. And here’s a clever technique that uses the srcdoc
attribute to get a similar result (but don’t forget to use the youtube-nocookie.com
domain).