I wanted re-usable ‘components’ on my Jekyll sites which I could use to render common chunks of content. These components would need to accept parameters that would change the layout or styling of the rendered HTML and also accept any HTML that I wanted rendered on the page. After trying a bunch of options I found that Liquid block plugins combined with Nokogiri was the best solution.

Here’s how I wanted it to work

No need to worry about writing markup or classes on my pages, just set a bunch of attributes and params and let the template do the rest:

# Call the component something like this:
<my-custom-component color=red>
	<heading>Big Red Text</heading>
	<content>
		Any kind of <strong>markup here</strong>.
	</content>
</my-custom-component>
# Have the markup look something like this:
<div>
	<h1 style="color: red">Big Red Text</h1>
	<div>
		Any kind of <strong>markup here</strong>.
	</div>
</div>

Liquid Blocks + Nokogiri is the answer

The following approach uses a Liquid Tag block type Jekyll plugin letting you pass in any pipeline separated params you might want, and any custom tags into the block. The tags are parsed by Nokogiri allowing for bug free HTML rendering with access to attributes if you need them.

# _plugins/foo/foo.rb
require "jekyll"
require "nokogiri"

module Foo
	class Tag < Liquid::Block
		def initialize(tag_name, input, parse_context)
			@params = parse_params(input.strip)
			super
		end

		def parse_params(input)
			result = {}
			input.split('|').each do | item |
				key = item.split('=').first
				value = item.split('=').last
				result[key] = value.delete_prefix('"').delete_suffix('"')
			end
			return result
		end

		def render(context)
			@text		= super
			elements	= Nokogiri::HTML::DocumentFragment.parse(@text)
			template	= "#{File.dirname(__FILE__)}/template.liquid"
			liquid 		= Liquid::Template.parse(File.read(template))
			variables	= {
				"params" => @params,
				"heading" => elements.search('heading').text,
				"content" => elements.search('content').to_html,

			}
			return liquid.render(variables)
		end
		Liquid::Template.register_tag "foo", self
	end
end

Your template files can stay nice and neat because all the logic and defaults are handled by Ruby in the plugin.

# _plugins/foo/template.html
<h1 class="{{ params.color }}">
	{{ heading }}
</h1>
{{ content }}

Call the block like this in your templates:

# page.html
{% foo color=red|size=large %}
	<heading>Big Red Text</heading>
	<content>
		<p>Any markup here</p>
	</content>
{% endfoo %}

Using Nokogiri to parse the content has an added benefit of providing access to attributes on any of the tags you pass into the content of a block. Here’s an example of a method I used to render a list of links.

def get_links(elements)
	items = []
		elements.search('links').each do | element |
			items << {
				"label" => element.text,
				"href" => element.attributes['href'].value,
			}
		end
	return items
end

The links were passed into the block like so:

{% foo %}
	<links href="/">Homepage</links>
	<links href="/about">About us</links>
	<links href="/contact">Contact</links>
{% endfoo %}

Why not just use includes?

Jekyll’s built in includes system was the first approach I tried and it does work but has some drawbacks. Your template would live in _includes and the parameters would be passed in using the standard method:

# _includes/my-custom-component.html
<div>
	<h1 style="color: {{ include.color }}">
		{{ include.heading }}
	</h1>
	<div>
		{{ include.body }}
	</div>
</div>
# page.html
{% include my-custom-component.html
	color="red"
	heading="Big Red Text"
%}

What about passing in HTML?

No problem, we can use the Liquid capture variable to pass in whatever we want:

{% capture body %}
	<p>I'm the body text</p>
{% endcapture %}

Need default parameters?

It’d be useful to have some defaults and we could use Liquid’s ‘default’ method to pull this off. If you only have a few params you can add them inline to your template but I find it’s easier to organise if you have them all listed at the top of the file:

# Inline your defaults if you only have a few
<h1>{% raw%}{{ include.heading | default: "Default Heading" }}</h1>
# List all defaults at the top if you have many
# Note that we renamed the variables here so you'll need to
# use 'heading' instead of 'include.heading' in the template.

{% if include.heading %}
    {% assign heading = include.heading %}
{% else %}
    {% assign heading = "Default Heading" %}
{% endif %}

{% if include.color %}
    {% assign color = include.color %}
{% else %}
    {% assign color = "red" %}
{% endif %}

<div>
	<h1>{{ heading }}</h1>
	...

All good so far but here’s where Jekyll includes fall short