After about two years of using this little trick I’m finally confident enough to say that it works the way I want. One of the big hassles of a static site generator is the content pipeline for things like images. A CMS like Drupal or Wordpress handles a lot of things like resizing and thumbnailing where Jekyll needs to provide its own method. This is one that I put together using Make along with a few other open source tools.

The first order of business is to get the absolute path to the current directory since we will be using it to route our processed image files. Additionally, Jekyll only takes files in the assets/ directory into account when building the site, so we will be storing our raw images in _images/ and then processing them into assets/ where Jekyll will pick them up during the build.

CURRENT_DIR = $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
JEKYLL_IMAGE_SRC_DIR = $(CURRENT_DIR)/_images
JEKYLL_IMAGE_DEST_DIR = $(CURRENT_DIR)/assets/images

Next is creating a list of targets in assets/ for our Makefile out of every image file we find in the _images/ directory. The implementation below only takes into account a single file extension but the find call could be expanded as needed to take into account, for example, multiple JPEG file endings.

JEKYLL_OPTIMIZED_JPGS = $(subst $(JEKYLL_IMAGE_SRC_DIR),$(JEKYLL_IMAGE_DEST_DIR),$(shell find $(JEKYLL_IMAGE_SRC_DIR) -type f -name '*.jpg'))
JEKYLL_OPTIMIZED_PNGS = $(subst $(JEKYLL_IMAGE_SRC_DIR),$(JEKYLL_IMAGE_DEST_DIR),$(shell find $(JEKYLL_IMAGE_SRC_DIR) -type f -name '*.png'))
JEKYLL_OPTIMIZED_SVGS = $(subst $(JEKYLL_IMAGE_SRC_DIR),$(JEKYLL_IMAGE_DEST_DIR),$(shell find $(JEKYLL_IMAGE_SRC_DIR) -type f -name '*.png'))
JEKYLL_OPTIMIZED_GIFS = $(subst $(JEKYLL_IMAGE_SRC_DIR),$(JEKYLL_IMAGE_DEST_DIR),$(shell find $(JEKYLL_IMAGE_SRC_DIR) -type f -name '*.gif'))

Next we want to define any option variables we will need for our different commands. This is a good place to decide how optimized you want your JPEGs or if you want to maintain Exif tags (you probably don’t).

JPEGOPTIM_OPTIONS = --max=65 --all-progressive --strip-all

Last before the real magic we create our targets which we will call from the CLI to actually optimize our images. They depend on our lists of optimized images or in the case of the catch-all every image optimizer we have implemented.

jpg-optimize: $(JEKYLL_OPTIMIZED_JPGS)

png-optimize: $(JEKYLL_OPTIMIZED_PNGS)

svg-optimize: $(JEKYLL_OPTIMIZED_SVGS)

gif-optimize: $(JEKYLL_OPTIMIZED_GIFS)

image-optimize: jpg-optimize png-optimize svg-optimize gif-optimize

Finally, we will use jpegoptim, optipng, rsync, and the ImageMagick suite to convert and optimize our images. I wasn’t able to find any good GIF or SVG optimizers so currently they are just a straight rsync copy to the output directory.

$(JEKYLL_IMAGE_DEST_DIR)/%.jpg: $(JEKYLL_IMAGE_SRC_DIR)/%.jpg
	mkdir -p $(dir $@)
	jpegoptim $(JPEGOPTIM_OPTIONS) --dest=$(dir $@) $^
	convert $@ -resize '100x100^' -gravity center -crop 100x100+0+0 +repage $(dir $@)thumbnail-$(notdir $@)
	rsync -a --ignore-existing $^ $@

$(JEKYLL_IMAGE_DEST_DIR)/%.png: $(JEKYLL_IMAGE_SRC_DIR)/%.png
	mkdir -p $(dir $@)
	optipng -out $@ $^
	convert $@ -resize '100x100^' -gravity center -crop 100x100+0+0 +repage $(dir $@)thumbnail-$(notdir $@)
	rsync -a --ignore-existing $^ $@

$(JEKYLL_IMAGE_DEST_DIR)/%.svg: $(JEKYLL_IMAGE_SRC_DIR)/%.svg
	mkdir -p $(dir $@)
	rsync -a --ignore-existing $^ $@

$(JEKYLL_IMAGE_DEST_DIR)/%.gif: $(JEKYLL_IMAGE_SRC_DIR)/%.gif
	mkdir -p $(dir $@)
	rsync -a --ignore-existing $^ $@

As you can see we are using generic rules to map files from the input to the output directory as well as maintain any directory structure from the input. After the images are optimized we also go ahead and create a 100x100px thumbnail image for all of our JPEG and PNG images in the assets/ directory, great for implementing a lightweight gallery!