All of the following code is collected in one Gist for reference.

I ended up going a little crazy with YouTube embeds on the site this week, and my load times suffered greatly. I had just added lazy loading for post images and seen some speed gains, but the Flash embeds were killing me. I needed the click-to-load feature that I’ve seen on other sites.

Here’s my solution. While I’m sure there are some perfectly good plugins available to do this, it had to start with markup, so wrote a tag plugin for Jekyll and worked from there.

The plugin is a modification of my earlier responsive YouTube plugin. Instead of inserting an object tag, it inserts a link element that I can style via CSS and hook via jQuery. It adds the needed information as data attributes on the tag, so parsing it later with jQuery is simple. Here’s the code, which can be easily modified to work with other services (for which I’d recommend creating separate Liquid tags).

a_lazy_youtube_tag.rbraw
# Title: Responsive Lazy Load YouTube embed tag for Jekyll
# Author: Brett Terpstra <http://brettterpstra.com>
# Description: Output a styled element for onClick replacement with responsive layout
#
# Syntax {% youtube video_id [width height] ["Caption"] %}
#
# Example:
# {% youtube B4g4zTF5lDo 480 360 %}
# {% youtube http://youtu.be/2NI27q3xNyI %}

module Jekyll
  class YouTubeTag < Liquid::Tag
    @videoid = nil
    @width = ''
    @height = ''

    def initialize(tag_name, markup, tokens)
      if markup =~ /(?:(?:https?:\/\/)?(?:www.youtube.com\/(?:embed\/|watch\?v=)|youtu.be\/)?(\S+)(?:\?rel=\d)?)(?:\s+(\d+)\s(\d+))?(?:\s+"(.*?)")?/i
        @videoid = $1
        @width = $2 || "480"
        @height = $3 || "360"
        @caption = $4 ? "<figcaption>#{$4}</figcaption>" : ""
      end
      super
    end

    def render(context)
      ouptut = super
      if @videoid
        # Thanks to Andrew Clark for the inline CSS calculation idea <http://contentioninvain.com/2013/02/13/video-embeds-for-responsive-designs/>
        intrinsic = ((@height.to_f / @width.to_f) * 100)
        padding_bottom = ("%.2f" % intrinsic).to_s  + "%"
        video = %Q{<a class="youtube" href="http://www.youtube.com/watch?v=#{@videoid}" data-videoid="#{@videoid}" data-width="#{@width}" data-height="#{@height}">YouTube Video</a>}
        %Q{<figure class="bt-video-container" style="padding-bottom:#{padding_bottom}">#{video}#{@caption}</figure>}
      else
        "Error processing input, expected syntax: {% youtube video_id [width height] %}"
      end
    end
  end
end

Liquid::Template.register_tag('youtube', Jekyll::YouTubeTag)

Next I styled the element and added a few rules to prepare for the object embed to occur later. This is the compiled CSS, but the Compass/Sass version is available in the Gist as well.

lazyyoutube.cssraw
.bt-video-container {
  margin-bottom: 3.5em;
  display: block;
  position: relative;
  height: 0; }
  .bt-video-container a {
    background: #ccc;
    position: absolute;
    color: #666;
    display: block;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    background-size: cover!important;
    text-decoration: none;
    text-align: center;
    padding-top: 25%; }
    .bt-video-container a:hover {
      text-decoration: none;
      color: #FFF; }
  .bt-video-container figcaption {
    position: absolute;
    left: 0;
    bottom: -50px;
    display: block;
    width: 100%;
    box-sizing: border-box;
    text-align: center; }

.bt-video-container-div {
  background: url(/images/youtube-play-button.png) center center no-repeat;
  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=85);
  opacity: 0.85;
  -webkit-transition: opacity 0.2s ease-in-out;
  -moz-transition: opacity 0.2s ease-in-out;
  -o-transition: opacity 0.2s ease-in-out;
  -ms-transition: opacity 0.2s ease-in-out;
  transition: opacity 0.2s ease-in-out;
  position: absolute;
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%; }

.bt-video-container-div:hover {
  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100);
  opacity: 1; }

.bt-video-container iframe,
.bt-video-container object,
.bt-video-container embed {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  margin-top: 0; }

Lastly, a quick jQuery script to handle the setup and replacement. This could be tied into a load-on-scroll plugin if you wanted to, but the performance gains were significant enough for my needs already. I have this compiled into my main JS file as part of a main object, but I made it standalone here. I think this should be working code…

jquery.lazyyoutube.jsraw
var youTube = (function() {
	'use strict';

	var youTube = {
		init: function() {
			$(".bt-video-container a.youtube").each(function(index) {
				var $this = $(this);

				var youtubeId = $this.data("videoid");
				$this.html(''); // empty any placeholders
				$this.prepend('<div class="bt-video-container-div"></div>&nbsp;');
				$this.css("background", "#000 url(http://i2.ytimg.com/vi/"+youtubeId+"/maxresdefault.jpg) center center no-repeat"); // Use poster image from YT as background
				
				
				var embedUrl = '//www.youtube-nocookie.com/embed/'+youtubeId+'?autoplay=1&rel=0'; // create an embed url.
				// no protocol
				// youtube-nocookie: prevent additional user tracking
				// autoplay: because user already clicked
				// rel=0: no "Related Videos" at end
				
				// set up the embed iframe
				var videoFrame = '<iframe width="'+parseInt($this.data("width"),10)+'" height="'+parseInt($this.data("height"),10)+'" style="vertical-align:top;" src="'+embedUrl+'" frameborder="0" allowfullscreen></iframe>';

				$this.click(function(ev){ // replace link with iframe on click
					ev.preventDefault();
					$this.replaceWith(videoFrame);
					return false;
				});
			});
		}
	}

	return youTube;

})();

// init at an opportune time
youTube.init();

To see it in action, visit any page on my site with a YouTube video.