How I Build This Site
(The following code snippets are a distillation of the most essential parts of my build system. You can see the full source here)
I chose to use GNU Make to generate this site.
In general, the method for using Make as an SSG is to take all your source files and apply transformations to them based on their file extensions.
For example, .md
files are processed into .html
files. Any files that do not need to be processed are copied.
The source files are stored in a separate directory from where the generated files will be placed.
The names of these directories are stored in the make variables $(source_d)
and $(target_d)
respectively.
The subdirectory structure of $(source_d)
is mirrored in $(target_d)
, so that $(source_d)/foo/bar.md
will become $(target_d)/foo/bar.html
.
Setup
The way I chose to implement this starts by generating two lists, one for the files in $(source_d)
and one for the subdirectories.
source_dirs := $(shell find -L $(source_d) -mindepth 1 -type d)
source_files := $(shell find -L $(source_d) -type f)
(In the code above -mindepth 1
prevents $(source_d)
itself from being included in the output of find
)
From here, all the pathnames in both $(source_dirs)
and $(source_files)
are duplicated with their top-level directory, $(source_d)
, substituted by the name of our target directory, $(target_d)
.
target_dirs := $(source_dirs:$(source_d)%=$(target_d)%)
target_files := $(source_files:$(source_d)%=$(target_d)%)
Then each filename listed in $(target_files)
has it’s filename extension substituted for an associated file extension.
The .md
to .html
example above is done by
target_files := $(target_files:%.md=%.html)
Rules
Now that we have lists of all the files we want to generate we can use them as dependencies for the default target and write rules for each pair of file extensions.
$(target_d)/%.html : $(source_d)/%.md
markdown "$(<)" | $(template) > "$(@)"
In the above example, we pipe the output of markdown
into a script whose name is stored in the variable $(template)
.
All $(template)
does is inject it’s stdin
into an html skeleton.
For this, I use a shell script with a heredoc.
A bare-bones implementation might look like this:
#! /bin/sh
cat << EOF
<html>
<header>
My Website
</header>
<article>
$(cat /dev/stdin)
</article>
<footer>
Copyright Me, Forever
</footer>
</html>
EOF
Similar rules are written for any other types of files that need processing.
$(target_dirs)
has it’s own rule that simply runs mkdir -p
for each directory.
A default rule will will run for any files in $(target_files)
that are not caught by another rule.
$(target_d)/% : $(source_d)/%
cp "$(<)" "$(@)"
This works because although the patterns would match all the files in both $(target_d)
and $(source_d)
, GNU Make will choose to run the pattern that matches most specifically.
For example, for the target file $(target_d)/foo/bar.html
, the pattern $(target_d)/%.html
is more specific than $(target_d)/%
, so the rule with $(target_d)/%.html
is run.
Cleanup
Once all of the targets are built all that’s needed is some clean up.
By default, if I were to move or remove a file in $(source_d)
, any previously generated file in $(target_d)
would still be there.
To fix this, I compile a list of all the files in $(target_d)
that are not listed in $(target_files)
or $(target_dirs)
:
old_files := $(filter-out $(target_dirs) $(target_files), $(shell find `$(target_d)` -mindepth 1 -depth -type f,d))
I use $(old_files)
as a dependency of the default target and have a rule that runs rm -rf
on each of it’s targets.
$(old_files) :
@if [["$(@)" == "/" ]]; then \
echo error: attempting to rm "$(@)"; \
else \
echo rm -rf "$(@)"; \
rm -rf "$(@)"; \
fi
The if-else clause checks if the target happens to be /
.
If it is then we echo an error message and skip it.
(This was added as a safeguard when that exact thing happened while refactoring and testing other parts of the code. Thankfully, I had backups.)
Sanity Checks
At the beggining of the makefile I include a series of preprocessor sanity checks.
They ensure that the needed variables, such as $(source_d)
and $(template)
, are set and that the files and folders they reference exist.
If any of them fail an error message is printed and make
exits.