← back

make: the ultimate static site generator

28 February 2017

After the Jekyll update last year rendered my blog inoperable, I finally managed to resurrect this site with a new static site generator1. It's fast, can generate pages from multiple source formats, is highly customizable and doesn't force me into using any one directory layout...

Presenting my < 80 line makefile that now powers this blog.


Include the GNU Makefile Standard Library

include ../scripts/gmsl

First, we define all the relevant paths

## Folders
SRC       := .
SUBDIRS   := posts
DST       := html
TEMPLATES := templates

and pandoc settings

## Pandoc settings
TOC       := --toc -V toctitle:'Contents'
CSS       := --css '/assets/style.css'
TEMPLATE  := --template $(TEMPLATES)/template.html $(CSS)

then find all the source files to be generated

MDS   := $(patsubst %.md,  %.html, $(wildcard $(SRC)/*.md))
ORG   := $(patsubst %.org, %.html, $(wildcard $(SRC)/*.org))
LHS   := $(patsubst %.lhs, %.html, $(wildcard $(SRC)/*.lhs))
HTMLS := $(patsubst %.html, %.htmlgen, $(wildcard $(SRC)/*.html))

In the process of writing this I learned that you can create "functions" in makefiles, which are just variables that use input parameters specified by position. This is the workhorse, which just calls pandoc passing it some parameters. For specifying various toggles and conditionals, the utility functions in gmsl are invaluable.

## Params:
## $<   -- input file name
## $(1) -- pandoc's input file format
## $(2) -- $(true) to turn off table of contents
define generate_html
	@pandoc -s -f $(1) $< -t html \
	$(TEMPLATE) \
	$(if $(call not $(2)), $(TOC)) \
	$(if $(call sne, $<, $(INDEX)), $(BACKTO)) \
	-o  $(DST)/$(notdir [email protected])
endef

Next, make rules. The top level rule $(TOPTARGETS) first runs make recursively in each subdirectory with the relevant goal, then runs the rules in this top level file. The trick here is that you can use variable references $(var) when defining a rule, which means that the value of the variable will be expanded in its place --- below, the $(TOPTARGETS) rule is actually a rule for all clean: ...

TOPTARGETS := all clean

.PHONY: $(TOPTARGETS) $(SUBDIRS) clean

$(TOPTARGETS): $(SUBDIRS) this
	@echo 'done!'

$(SUBDIRS):
	@echo 'building subdir [email protected]...'
	@$(MAKE) -C [email protected] $(MAKECMDGOALS)
	@echo 'building root...'

this: $(MDS) $(ORG) $(LHS) $(HTMLS)

Then some pattern rules to run pandoc on each different input file format:

%.htmlgen: %.html
	@echo 'html file: $<'
	$(call generate_html, markdown_strict+yaml_metadata_block)
	@mv	$(DST)/[email protected] $(DST)/$<

%.html: %.lhs
	$(call generate_html, markdown+lhs, 'notoc')

%.html: %.md
	$(call generate_html, markdown, notoc)

%.html: %.org
	$(call generate_html, org, 'notoc')

Finally, to upload I just rsync --- the nice thing here is that I never generate a output folder with a copy of all my static assets; instead those are copied to the server directly here.

upload:
	(rsync -avz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
	--progress ./docs ./assets ./html/* \
	myblogserver)

clean:
	@rm $(DST)/*

Problems

Probably these will be solved in a followup to this blog post by some Haskell scripts when I have more time...


  1. For nearly two years I've been toying with building my own, but just found it too complex to design and tedious to build in one sitting. So this minimalist approach works, because I actually got it done.