Jekyll and Make: The Even Lazier Developer
Spring is in the air again and the young developer finds his old pile of JavaScript just does not want to build anymore. So, in a fit of reason, he decides that old school is new again and that GNU Make at least has good documentation. Yes, you heard correctly, what was once 200 lines of JS configuration is now a 20 line Makefile.
Since Our Last Episode
There have been a few changes in the world of Jekyll since I last really explored setting up an automated build. Better asset handling and the inclusion of gem themes are probably the biggest changes but luckily the Minima theme is easy to customize and the documentation is still quite good.
The world of JavaScript has changed even more, resulting in a Gruntfile that wouldn’t build the site anymore filled with plugins that replicated features Jekyll already had. So…I pitched it. Threw the whole thing out, and started over using GNU Make instead. It might sound crazy, but keep reading, you might end up surprised.
The Make-ings of Greatness
You’re probably thinking, “Make…that sounds familiar. I think I used it once
for a project in C or to build a package for my distro, what’s it got to do with
a blog?” Simple, it’s a tool that lets you create lists of tasks and execute
them based on dependencies. That makes it perfect for building resources and
keeping complex tasks in order while you focus on your site’s content. The other
benefit is if you’re going for a CI/CD setup, using a Makefile can keep your
local and CI build instructions in sync more easily. No need to update the
script in both places, just call make build
for both!
Starting out from a blank Jekyll site is easy with the Makefile below. You can omit the presentation and clone information if you like but I’ll be using it later to highlight the flexibility of Make.
CURRENT_DIR = $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
PRESENTATION_REPO_URL = git@example.com:thomascfoulds/presentations.git
PRESENTATION_DIR = $(CURRENT_DIR)/assets/presentations
JEKYLL_SERVE_OPTS = --drafts
clone:
ifneq "$(wildcard $(PRESENTATION_DIR)/*)" ""
cd $(PRESENTATION_DIR) && git pull origin master
else
git clone $(PRESENTATION_REPO_URL) $(PRESENTATION_DIR)
endif
clean: configure
bundle exec jekyll clean
configure:
bundle install --path=vendor
bundle update
serve: clean
bundle exec jekyll serve $(JEKYLL_SERVE_OPTS)
build: clean clone
JEKYLL_ENV=production bundle exec jekyll build
Anatomy of a Makefile
First of all, I should direct you to the very in depth Make documentation which will do a far better job than I will describing all the different options available in Make. In any case, let’s break apart this Makefile into its various parts.
CURRENT_DIR = $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
PRESENTATION_REPO_URL = git@example.com:thomascfoulds/presentations.git
PRESENTATION_DIR = $(CURRENT_DIR)/assets/presentations
JEKYLL_SERVE_OPTS = --drafts
Most examples you’ll see open the Makefile with a collection of variables that
are used throughout the various tasks. In this case I’m setting up compiler
options of Jekyll and setting up some paths for my slideshow repository. You can
also reference variables you’ve defined previously using the $(VARIABLE_NAME)
syntax to build up longer values out of smaller parts. This can be especially
helpful in making your Makefile portable to systems and users other than
yourself.
configure:
bundle install --path=vendor
bundle update
The task or target is the smallest useful part of a Makefile and describes a
series of actions to achieve a goal. The goal can be anything from compiling an
executable to making sure your dependencies are up to date like you see here.
This target is named configure
and would be called from the commandline with
make configure
. That would install and and update any gems I have listed in
my Gemfile
while printing the command output to the screen. If either command
fails, exiting with a nonzero status, Make is also smart enough to stop running
and report the error. While that isn’t super useful for a simple task like this
it’s a lifesaver for more complex tasks that are dependant on eachother. You
don’t need to worry about a compiler error early on corrupting your final
product.
clone:
ifneq "$(wildcard $(PRESENTATION_DIR)/*)" ""
cd $(PRESENTATION_DIR) && git pull origin master
else
git clone $(PRESENTATION_REPO_URL) $(PRESENTATION_DIR)
endif
So the first difference you probably noticed between this task and the one
before is that some of the commands aren’t indented to the same level. Make
uses whitespace to determine what is a command meant for it and which is
meant to build the target. It also specifically requires tab, \t
, characters
for its indentation by default. Spaces may look the same but will confuse Make,
so be sure your editor isn’t expanding tabs by default! In this case, we’re
using Make to check if a directory has any contents and if not we clone down my
collection of presentation slideshows from another Git repo. If it does exist,
instead we switch into the directory and pull down the latest commits to the
master branch. Now you might wonder why the cd $(PRESENTATION_DIR)
is on the
same line as everything else. Why not split it into two lines, wouldn’t it be
cleaner? Actually, Make executes each line in it’s command list in its own shell
context and in the same directory as the Makefile
. So to actually be in the
right directory to do our Git tasks we have to change directory and run them all
on the same line.
clean: configure
bundle exec jekyll clean
serve: clean
bundle exec jekyll serve $(JEKYLL_SERVE_OPTS)
build: clean clone
JEKYLL_ENV=production bundle exec jekyll build
Now we get to see the real power of Make with its dependency handling. Here we
have three tasks that all have dependencies defined in <target>: <dependency>
format. What this means is that when we run make clean
it will look through
the Makefile
to find the configure
task and run it first before trying to
run any of the clean
commands. Great news because without our gems we’re not
going anywhere fast! Multiple dependencies are just as easy, list them out space
separated after the target name or put them in a variable and include them in
with a $(DEPS)
.
Building the Site
Putting it all together is the final step to getting your site running smoothly.
I like feeling like an 80’s Hollywood hacker so I draft my posts with a
combination of tmux and NeoVim in a terminal window which
makes having a split running make serve
very natural. But if that isn’t your
idea of a good time any editor will work and you can keep the terminal window in
the background working for you. Once you’re done writing and are ready to push
your site to the server it’s time to use our last Make target. Running
make build
will clean up any leftovers in your _site
directory and pull down
the latest slideshows before running a production Jekyll build. The JEKYLL_ENV
variable can be useful for selectively including additional templates like
analytics which you’d rather not deal with in a development environment. Once
Make finishes building your site you’re ready to copy your _site
directory up
to a webserver for the world to see.
What’s Next?
At this point we have a site that builds reliably and a system which will handle dependencies for us as we add more tasks, a great starting point. We’re still missing an asset pipeline for images, a secure webserver, and automated deploys of new content out to the server. Check back soon for how to configure nginx to serve our Jekyll site, optimize images for the web, and set up CI builds to make posting new content just a Git commit away!