My journey building simple plugins to modify and add to the Jekyll build process.
Motivation and context
What is Jekyll?
To keep it brief Jekyll is an SSG (Static Site Generator). It’s used by Github for Github pages and it’s what was used to build the site you’re seeing right now!
This post will assume you have used Jekyll.
The problem(s)
As I have been creating this blog I’ve found a desire to extend and modify the Jekyll build process.
- Resizing/formatting images for web
- PostCSS processing
- Other PostCSS processing
- Building images for Open Graph meta data
Additionally, assets (such as images) are not located relative to individual
blog posts but are lumped together at the top of assets/
.
The result is either
- living with all assets at top level and risking assets with the same name overwriting one another
- creating a system of subfolders which need to referenced for each blog post
After attempting to reference an image for the fourth time via
1
![](assets/images/2021-06-28-image-resizing/dog.jpg)
then trying
1
2
{% assign img_path = "assets/images/2021-06-28-image-resizing/" %}
![]({{ "dog.jpg" | prepend img_path }})
I decided something more complex is necessary.
Solutions
A naive solution would be to create a script that ran the desired programs after Jekyll had generated the necessary files. It would look something like this,
1
2
3
4
5
6
7
8
9
#/usr/bin/env sh
# ... preprocessing steps
optmise_media assets/media/*.*
bundle exec jekyll build
# ... postprocessing steps
minify assets/js/*.*
Unfortunately this script would be losing out on the live reload feature
jekyll serve
provides. Performing manual rebuilds is not the end the world
but it is a pain point.
What is already available
There was a Jekyll plugin but it was quite old. It makes use of Imagemagick to resize images to whatever size is defined as a filter parameter.
1
2
It uses a filter like so
{{ "path/to/assets/image.png" | resize: "800x800>" }}
Using Imagemagick has the additional benefit You can use whatever you want as the second parameter as long as Imagemagick can read it.
My solution
Image optimisation
image_optim
is a Ruby library that
contains a variety of optimisation utilities.
Running this library across all the images after they have all been generated could be an easy first step.
Since I’m not creating a gem for this first plugin I have to create my script
under the _plugins/
folder which needs to be created. Based on
the Jekyll plugin installation documentation.
In order to import image_optim
I appended this package to the bottom of my
Jekyll project’s Gemfile
.
1
2
3
4
5
6
7
8
9
# ... the other stuff
# For syntax highlighting
gem 'rouge'
# For /_plugins/
gem 'image_optim'
gem 'image_optim_pack'
gem 'rmagick'
The Jekyll Hooks makes it very easy to run code at the right point in the build process. In this case I perform a pass on all the images and optimise them in place.
1
2
3
4
5
6
require 'image_optim'
Jekyll::Hooks.register :site, :post_write do
image_optim = ImageOptim.new
image_optim.optimize_images!(Dir['_site/assets/images/*/*.*'])
end
The best part, it reruns whenever the site live reloads!
The worrying part, it reruns whenever the site live reloads! If the number
of images increases a lag spike could become apparent whenever we build our
site this would be especially pronounced for local development. From my
understanding, Jekyll will copy the unoptimised images from the top level
/assets/
folder to the /_site/
folder each reload therefore it isn’t
possible to improve the performance of this plugin without turning it off
completely.
¯\_(ツ)_/¯ given the amount of extra effort needed and the minor reward to solve this I think I can throw this on the backlog for future Josh.
We’re going to use this handsome fellow from pxfuel.com for comparisons.
1
2
3
4
5
6
7
# original image
du -h assets/images/2021-06-28-image-resizing/dog.jpg
# 236K
# image optimised
du -h _site/assets/images/2021-06-28-image-resizing/dog.jpg
# 148K
This outcome was obtained after tweaking some of the image_optim
settings to
run lossy jpeg compression with the lowest quality option. The image above is the
148K version and it looks perfectly fine.
As you saw, the above is the entire plugin for adding a new, simple step to the build process. Huge props to Jekyll for making changes like this is as simple as writing a script.
Image resizing
The next stage was to control the size of images. I could have copied the
jekyll-resize
gem and called it a day but there were two aspects of the old
plugin I wanted to improve.
The first aspect was discussed in The problem(s). Having to write out links to grab the image you want gets in the way of writing and disincentives adding visuals to blog posts.
The second was how granular image resizing was. As I write posts I don’t want to think about exact image sizes and how they will fit with page composition, I want quick defaults and further tweaking later on.
The solution I propose
1
2
It uses a filter like so
{{ "image.png" | resize: "large" }}
In order to resize the images I used rmagick
which had decent
documentation. Resizing was an easy process
Expand for embed_asset_v1.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
require 'mini_magick'
module Jekyll
##
# contains the embed_image filter
# Usage: {{ "path/to/image" | embed_image: "large" }}
module MediaManagementFilters
def embed_image(filepath, size = nil)
return filepath if size.nil?
img = MiniMagick::Image.open(".#{filepath}")
limit = !named_sizes.nil? && named_sizes.key?(size) ? named_sizes[size] : Integer(size)
if img.width <= limit
puts "Original image #{filepath} smaller than the #{size} specification"
return filepath
end
dir = "#{File.dirname(filepath)}/"
filename = File.basename(filepath)
resized_filename = "gen-#{limit}-#{filename}"
resized_filepath = "#{dir}#{resized_filename}"
# This part derived from jekyll-resize
if !File.exist?(".#{resized_filepath}") || File.mtime(".#{resized_filepath}") <= File.mtime(".#{filepath}")
img.resize(limit.to_s)
img.write(resized_filepath)
site = @context.registers[:site]
# Jekyll has already scanned for static files (to copy to _site later)
# Need to add these static resources manually
site.static_files << Jekyll::StaticFile.new(site, site.source, dir, filename)
end
resized_filepath
end
end
end
Liquid::Template.register_filter(Jekyll::MediaManagementFilters)
The code above matches the feature set of the old jekyll-resize
plugin. But now
allows for some words to map to values. In my _config.yml
I have
1
2
3
4
5
6
7
8
# stuff...
image_sizes:
large: 900
medium: 450
small: 200
# other stuff...
The site.static_files << Jekyll::StaticFile.new(site, site.source, dir,
filename)
on line 31 of embed_asset_v1.rb
was there to let Jekyll know of
new static resources. From my understanding a scan of the available static
resources is performed early on. The resources generated via this liquid filter
have to be manually added.
Image location
Looking good so far but I still have to specify a verbose path to find the desired image.
1
{{ "path/to/assets/image.png" | embed_image: "large" }}
The technical problem, the filter did not let me know what page I was compiling; not in any obvious way.
After searching online and printing the contents of a bunch variables Jekyll
provided I noticed that the @context.registers
contained more than just the
site-wide context. @context.registers[:page]
which is of class Drop
looked
like what I was looking for.
Attempting to print this object resulted in a recursion limit error which was a huge pain. My current hypothesis, the object attempts to evaluate and print each of it’s variables which might start recursively printing. This would not be a problem if the site structure was like a tree without loops, unfortunately this is not the case. This comment in the Jekyll repo indicates the same. There was probably a better way to print the objects but I ended up printing the keys (which did not cause a recursion issue) and accessing the underlying value.
'slug'
(the url friendly version of the title but essentially the title if the original is url friendly) contained the named portion of the filename and 'date'
was the value
prepended to produce %Y-%m-%d-{slug}
which is the filename of the post. After
a little bit of refactoring I ended up with the following,
Expand for embed_asset_v2.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# frozen_string_literal: true
require 'mini_magick'
module Jekyll
##
# contains the embed_image filter
module MediaManagementFilters
##
# the filter embed_media prepends /assets/media/Y-m-d-page-name/ to the input
# provide a page to make the path relative to that page instead
def embed_media(filename, page = nil)
"#{media_dir page}#{filename}"
end
##
# performs embed_asset on the input then generates resized versions of the file
def embed_image(filename, size = nil, page = nil)
dir = media_dir page
return "#{dir}#{filename}" if size.nil?
img = MiniMagick::Image.open("./#{dir}#{filename}")
site = @context.registers[:site]
limit = image_size(size, site.config['image_sizes'])
return "#{dir}#{filename}" if img.width <= limit
resized_filename = save_image(img.resize(limit.to_s),
site, dir, "gen-#{limit}-#{filename}",
File.mtime("./#{dir}#{filename}"))
"#{dir}#{resized_filename}"
end
private
def media_dir(page = nil)
page ||= @context.registers[:page]
page_name = page.key?('date') ? "#{page['date'].strftime('%Y-%m-%d')}-#{page['slug']}" : page['slug']
"/assets/media/#{page_name}/" # path between site.source and the file
end
def image_size(size, named_sizes = nil)
# this is the width limit
!named_sizes.nil? && named_sizes.key?(size) ? named_sizes[size] : Integer(size)
end
##
# If the image is older than mtime then it gets rewritten
def save_image(img, site, dir, filename, mtime = Time.new(0))
fullpath = "#{site.source}#{dir}#{filename}"
if !File.exist?(fullpath) || File.mtime(fullpath) <= mtime
img.write("#{site.source}#{dir}#{filename}")
# Jekyll has already scanned for static files (to copy to _site later)
# Need to add these static resources manually
site.static_files << Jekyll::StaticFile.new(site, site.source, dir, filename)
end
filename
end
end
end
Liquid::Template.register_filter(Jekyll::MediaManagementFilters)
The filter syntax now has the additional property page
. You may want to
reference an image from another page. Since the post
objects used in
site.posts
are of the same type as the one returned by
@context.registers[:page]
we can specify an explicit page to make our path
relative to.
I added embed_media
which only performed the path expansion portion. Items like
SVGs can benefit from the filter without rmagick
attempting to shrink it.
Conclusion
Prior to this blog post I had no idea about any Ruby syntax or what made Ruby tick. Learning about the syntax and the way objects and classes work has been very interesting. rubylearning.com was a great resource to get up to speed quickly.
The plugins written remain in my _plugins/
folder for now. The current
approach is very opinionated on how assets should be organised and I suspect
they scripts won’t be very helpful as installable gems.
For now I encourage people who would like to use these scripts to create a
_plugins
folder and modify them yourself. If there is some interest I’ll
look into packaging them into gems.
This is how I am currently using my new filter in this website.
1
2
3
4
On the front page
<img src="{{ post.image | embed_image: "small", post }}" alt=""/>
or in this post
![Small picture of a dog]({{ "dog.jpg" | embed_image: "small" }})