her.esy.fun/src/posts/0018-makefile-as-static-site-builder-follow-up/index.org
2021-05-09 17:32:15 +02:00

6.2 KiB

Makefile as static site builder

After many different tools, I recently switched to a simple Makefile to generate my static website. In Static Blog Builder I give a starter pack. Along the way I had to learn about Makefiles. So here are a few pointers and helpers.

So an important one. The first rule of your Makefile will be the default rule. I called mine all which will depends on another rule call site. Why? Because, the rule format is generally something like:

file_to_generate: file_to_use another_file_to_use
	build --input file_to_use another_file_to_user \
		--output file_to_generate

if file_to_generate does not exists, then make will look at its dependencies. If any of its dependency need to be updated, it will run all the rules in the correct order to rebuild them, and finally run the script to build file_to_generate. A file need to be updated if one of its dependency need to be updated or is newer.

The ususal case of make is about building a single binary out of many source files. But for a static website, we need to generate a lot of files from a lot of files. So we construct the rules like this:

all: site

# build a list of files that will need to be build
DST_FILES := ....
ALL += $(DST_FILES)

# another list of files
DST_FILES_2 := ....
ALL += $(DST_FILES_2)

site: $(ALL)

In my Makefile I have many similar block with the same pattern.

  1. I retrieve a list of source files
  2. I construct the list of destination files (change the directory, the extension)
  3. I declare a rule to construct these destination files
  4. I add the destination files to the ALL variable.

So I have a block for:

  • raw assets I just want copied
  • images I would like to compress for the web
  • html I would like to generate from org mode files
  • gmi I would like to generate from org mode files
  • xml files I use as cache to build different index files
  • index.html file containing a list of my posts
  • rss.xml file containing a list of my posts
  • gemini-atom.xml file containing a list of my posts

So to go further, let's take a look at a simplified raw assets copy block:

SRC_ASSETS := $(shell find src -type f)
DST_ASSETS := $(patsubst src/%,_site/%,$(SRC_ASSETS))
_site/% : src/%
	@mkdir -p "$(dir $@)"
	cp "$<" "$@"
ALL += $(DST_ASSETS)

OK, this looks terrible. But mainly:

  1. SRC_ASSETS will contains the result of the command find.
  2. We replace all src/ prefix of all those files by the _site/ prefix.
  3. We create a rule, if you are asked to build _site/<something> look at src/<something> and

    • create the directory to put _site/<something> in
    • copy the file

About the line @mkdir -p "$(dir $@)":

  • the @ at the start of the command simply means that we make this execution silent.
  • The $@ is replaced by the target string.
  • And $(dir $@) will generate the dirname of $@.

For the line with cp you just need to know that $< will represent the first dependency.

Once you have this pattern in mind. Adding new block become a bit natural. You will also like to use some variables for repetitive names.

Prelude

all: site
SRC_DIR ?= src
DST_DIR ?= _site
CACHE_DIR ?= .cache

# we don't want to publish files in drafts
NO_DRAFT := -not -path '$(SRC_DIR)/drafts/*'
# we don't copy source files
NO_SRC_FILE := ! -name '*.org'

CSS

# CSS
SRC_CSS_FILES := $(shell find $(SRC_DIR) -type f -name '*.css')
DST_CSS_FILES := $(patsubst $(SRC_DIR)/%,$(DST_DIR)/%,$(SRC_RAW_FILES))
ALL += $(DST_CSS_FILES)
$(DST_DIR)/%.css : $(SRC_DIR)/%.css
	@mkdir -p "$(dir $@)"
	minify "$<" > "$@"
css: $(DST_CSS_FILES)

This is very similar to the block for raw assets. The difference is just that instead of using cp we use the minify command. And also I use global constants (SRC_DIR and DST_DIR).

ORG -> HTML

Now this one is more complex but is still follow the same pattern.

# ORG -> HTML
EXT ?= .org
SRC_PANDOC_FILES ?= $(shell find $(SRC_DIR) -type f -name "*$(EXT)" $(NO_DRAFT))
DST_PANDOC_FILES ?= $(patsubst %$(EXT),%.html, \
                        $(patsubst $(SRC_DIR)/%,$(DST_DIR)/%, \
                            $(SRC_PANDOC_FILES)))
PANDOC_TEMPLATE ?= templates/post.html
MK_HTML := engine/mk-html.sh
PANDOC := $(MK_HTML) $(PANDOC_CSS) $(PANDOC_TEMPLATE)
$(DST_DIR)/%.html: $(SRC_DIR)/%.org $(PANDOC_TEMPLATE) $(MK_HTML)
	@mkdir -p "$(dir $@)"
	$(PANDOC) "$<" "$@.tmp"
	minify --mime text/html "$@.tmp" > "$@"
	@rm "$@.tmp"
ALL += $(DST_PANDOC_FILES)
html: $(DST_PANDOC_FILES)

So to construct DST_PANDOC_FILES this time we also need to change the extension of the file from org to html. We need to provide a template that will be passed to pandoc.

And of course, as if we change the template file we would like to regenerate all HTML files we put the template as a dependency. But importantly not at the first place. Because we use $< that will be the first dependency.

I also have a short script instead of directly using pandoc. Because I would like to handle the toc depending on the metadatas in the file.

The mk-html.sh is quite straightforward:

#!/usr/bin/env bash
set -eu

# put me at the top level of my project (like Makefile)
cd "$(git rev-parse --show-toplevel)" || exit 1
template="$1"
orgfile="$2"
htmlfile="$3"

# check if there is the #+OPTIONS: toc:t
tocoption=""
if grep -ie '^#+options:' "$orgfile" | grep 'toc:t'>/dev/null; then
    tocoption="--toc"
fi

set -x
pandoc $tocoption \
       --template="$template" \
       --mathml \
       --from org \
       --to html5 \
       --standalone \
       $orgfile \
       --output "$htmlfile"

Once generated I also minify the html file. And, that's it. But the important part is that now, if I change my script or the template or the file, it will generate the dependencies.