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.
Developer & Programmer with +15 years professional experience building software.
Seeking WFH, remoting or freelance opportunities.