How to create customizable Liquid tags in Jekyll

Creating custom Liquid tags in Jekyll to simplify the injection of code into static pages

I rebuilt my blog recently using Jekyll. In the process of migrating from blogspot.com I came across a few issues and limitations in the Liquid templating system that Jekyll is built on.

Two of these limitations were inserting Google Ads code and formatting inline image captions and description text. I didn't want to be constantly copy pasting a large chunk of HTML and needed a solution that did all that for me.

Custom Liquid tags

The Liquid templating language that Jekyll relies on has a few built in tags but they are both surprisingly few and generic.

Tags are the code that goes between the {% and %} tags. Example:

{% if %}
{% else %}
{% endif %}

Luckily you can create your own tags. It is easy and the cleanest way to inject and manipulate your static site code.

All custom tags should be saved with the extension .rb into a folder called /_plugins/ in the root of your site. You need to restart the Jekyll serve each time you make changes to these files to reload your plugins.

Custom Google Ads Tag

I wanted a simple instruction that would inject the ads code into my page everywhere I put it.

Given the tag code below I can now write {% ads %} directly into my blog posts as many times as I want and have Jekyll inject the requested Google ads code.

class AdsInlineTag < Liquid::Tag
  def initialize(tag_name, input, tokens)
    super
  end

  def render(context)
    # Write the output HTML string
    output =  "<div style=\"margin: 0 auto; padding: .8em 0;\"><script async "
    output += "src=\"//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js\">"
    output += "</script><ins class=\"adsbygoogle\" style=\"display:block\" data-ad-client=\"xxxxx\""
    output += "data-ad-slot=\"yyyyyy\" data-ad-format=\"auto\"></ins><script>(adsbygoogle ="
    output += "window.adsbygoogle || []).push({});</script></div>"

    # Render it on the page by returning it
    return output;
  end
end
Liquid::Template.register_tag('ads', AdsInlineTag)

But what if we want to sometimes display ads from a different slot or client?

You could create a tag for each variation or...

Accepting Tag Parameters

A better approach than copy/pasting your code for every combination of values would be to extend the tag code to accept parameters through the input variable.

One slight problem is that there is only one input parameter permitted in tags. A simple way around this is to split the input on a known separator.

In the example below the tag accepts both the ad-client number and the ad-slot number as a pipe (|) separated input value:

Allowing us to do this:
{% ads ca-pub-00000000000000|555555555 %}

class AdsInlineTag < Liquid::Tag
  def initialize(tag_name, input, tokens)
    super
    @input = input
  end

  def render(context)
    # Split the input variable (omitting error checking)
    input_split = split_params(@input)
    adclient = input_split[0].strip
    adslot = input_split[1].strip

    # Write the output HTML string
    output =  "<div style=\"margin: 0 auto; padding: .8em 0;\"><script async "
    output += "src=\"//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js\">"
    output += "</script><ins class=\"adsbygoogle\" style=\"display:block\" data-ad-client=\"#{adclient}\""
    output += "data-ad-slot=\"#{adslot}\" data-ad-format=\"auto\"></ins><script>(adsbygoogle ="
    output += "window.adsbygoogle || []).push({});</script></div>"

    # Render it on the page by returning it
    return output;
  end

  def split_params(params)
    params.split("|")
  end
end
Liquid::Template.register_tag('ads', AdsInlineTag)

Configure Tags with JSON

The pipe separator trick quickly becomes hard to maintain. Not only are the parameter values cryptic but their order also matters. This makes it difficult to only change the adslot variable as you must pass in an empty or the default adclient value every time as the first value.

By supporting a JSON formatted parameter data we can pass in a more complicated and flexible configuration.

Customizing both ad-client and ad-slot:
{% ads {"adclient":"ca-pub-00000000000000", "adslot":"555555555"} %}

only modifying ad-slot
{% ads {"adslot":"8888888"} %}

still support the default with no parameters:
{% ads %}

require 'json'

class AdsInlineTag < Liquid::Tag
  def initialize(tag_name, input, tokens)
    super
    @input = input
  end

  def render(context)

    # Set defaults first, replace with your values!
    adclient = "xxxxxx"
    adslot = "yyyyy"

    # Attempt to parse the JSON if any is passed in
    begin
      if( !@input.nil? && !@input.empty? )
        jdata = JSON.parse(@input)
        if( jdata.key?("adclient") )
          adclient = jdata["adclient"].strip
        end
        adslot = jdata["adslot"].strip
      end
    rescue
    end

    # Write the output HTML string
    output =  "<div style=\"margin: 0 auto; padding: .8em 0;\"><script async "
    output += "src=\"//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js\">"
    output += "</script><ins class=\"adsbygoogle\" style=\"display:block\" data-ad-client=\"#{adclient}\""
    output += "data-ad-slot=\"#{adslot}\" data-ad-format=\"auto\"></ins><script>(adsbygoogle ="
    output += "window.adsbygoogle || []).push({});</script></div>"

    # Render it on the page by returning it
    return output;
  end
end
Liquid::Template.register_tag('ads', AdsInlineTag)

Accessing post variables in tags

The inline image tag was a little bit more tricky as the tag class needed to be able to pull data out of the post page it was being rendered in (for the site baseurl parameter).

Luckily there is a clever way to do that:

class ImageWithCaptionTag < Liquid::Tag
  def initialize(tag_name, input, tokens)
    super
    @input = input
  end

  # Lookup allows access to the page/post variables through the tag context
  def lookup(context, name)
    lookup = context
    name.split(".").each { |value| lookup = lookup[value] }
    lookup
  end

  def render(context)

    # Accessing the page/site variable for the base url
    baseurl = "#{lookup(context, 'site.baseurl')}"

    # Reading the tag parameter (using the pipe-split technique)
    input_split = split_params(@input)    
    img_path = input_split[0].strip.downcase
    # Caption is an optional second parameter
    if( input_split.length > 1 )
      caption = input_split[1].strip
    end

    # Create the HTML output for the image container with an optional caption
    # the 'captioned-image' css class controls the look and feel of the image
    # in my case the class centeres the image and poses maximum size restrictions
    output =  "<div class=\"captioned-image\"><div>"
    output += "<img src=\"#{baseurl}/#{img_path}\" alt=\"#{caption}\">"
    if( !caption.nil? && !caption.empty? )
      output += "<p>#{caption}</p>"
    end
    output += "</div></div>"

    return output    
  end

  def split_params(params)
    params.split("|")
  end
end
Liquid::Template.register_tag('imgc', ImageWithCaptionTag)

This allows me to write code such as:

{% imgc img/picture.jpg|This is a image caption text. %}

The inline image tag still uses our simple pipe character trick to separate different input values. However this tag can be augmented with the JSON technique discussed above to achieve a more flexible configuration.

Using the tag techniques discussed in this post will allow you to create your own tags for almost every use. Tags can be very powerful and allow you great flexibility in your site and rendered output.

If you find yourself pasting HTML code between posts it might be time to create a tag to do that work for you.



Software Developer
For hire


Developer & Programmer with +15 years professional experience building software.


Seeking WFH, remoting or freelance opportunities.