Update Aug 2026: This is no longer how I generate this website. The setup described here, while somewhat, turned out a bit too complicated for my taste. I now generate my site using only POSIX shell (and a markdown to HTML converter, obviously).
Nov 2023
In this byte, I want to briefly show the setup I use to generate my website. It is a very bespoke solution, but that is not really a bad thing in my view.
I am very much in favor of using a static site generator whenever possible. Using heavy-duty CMSs for personal websites is absurdly overkill and hosting some dumb HTML files is cheap, easy, more secure.
But choosing between the gazillon different static site generation tools out there is a daunting task. A big problem is—it seems to me—that these tools usually have some happy-path that works really well and is super easy to use, but as soon as you stray from that path you’re in for a weird ride of workarounds and hacks. To make the most common use-cases as easy as possible these tools often favor convention over configuration but these conventions can be problematic if they are poorly documented or don’t mesh well with your own use-case and preferences.
The spectrum in maturity, quality, and level of (required) configuration is also considerable among all the static site generators out there.
Maybe there are some great tools out there, I wouldn’t know, as it’s impossible to try them all. The ones I’ve used beyond a quick trial run are Wintersmith, Gatsby, and Eleventy. These are quite different from one another but they share being based on JavaScript. A couple of months ago I was looking for a tool that didn’t depend on any scripting language other than shell and so I went and assembled a little setup that is simple, yet flexible—at least for what I need it to do.
make websiteThe setup I have is quite simple and only uses Make as the buildtool, M4 as a lowtech templating engine, Lowdown to parse Markdown files, and some small shellscripts.
Document sources are written in Markdown and include a block of metadata in the beginning. This very document e.g. starts with:
template: byte
title: How to generate a website using Make and M4
date: 2023-11
In this byte, I want to briefly show the setup I use to generate my
website. It is a very bespoke solution, but that is not really a bad
thing in my view.
Simply converting that document to HTML would only leave us with the text content, in this case:
<p>In this byte, I want to briefly show the setup I use to generate my
website. It is a very bespoke solution, but that is not really a bad
thing in my view.</p>
But of course I want to embed this inside a document that also includes a header with navigation elements, a generic site footer, and maybe displays some of the document’s metadata in some specifc way. For that, I use HTML templates that include some M4 directives.
E.g. the byte.m4 template displays the main content using this code:
<main>
<article>
<header>
<h2>M_title</h2>
<time>M_date</time>
</header>
[[include(M_body)dnl]]
</article>
</main>
As a convention, I prefix all constants that I will later define and
that M4 replaces with M_. The constants [[M_title]] and
[[M_date]] in the snippet correspond to the metadata keys in the
Markdown document. We will later set [[M_body]] to be the file name
of the main body contents.
We can have some conditional logic, e.g. in the HTML head we can point
to an RSS feed, if the corresponding [[M_feed]] macro is defined:
[[ifelse(M_feed,,, <link rel="alternate" type="application/xml+rss" href="M_feed" />)]]
Every metadata key that was defined in a Markdown file can be used in
the template, and the template key determines which template file is
used. How does the metadata get from the markdown file to M4 though?
For each Markdown sourcefile Make calls a shell script that generates a
“partial” HTML file and a second file with M4 macros. The partial HTML
is created by Lowdown and contains the main body of the document that is
then to be embedded into some kind of template. The script loops over
the metadata keys in the Markdown file (Lowdown recognizes key-value
pairs at the beginning of a file) and converts them to M4
definitions. It writes these definitions to a file followed by an
include directive to include the template file.
The whole script is just this:
mdfile=$1
metakeys=$(lowdown -L $mdfile)
macrofile=${mdfile%.md}.part.m4
echo "[[include(common.m4)dnl]]" >$macrofile
for key in $metakeys ; do
echo "[[define([[M_$key]], [[$(lowdown -X$key $mdfile)]])dnl]]"
done >>$macrofile
echo "[[include(M_template.m4)dnl]]" >>$macrofile
lowdown $mdfile
You can probably guess that lowdown -L lists all metadata keys in a
file, and lowdown -Xkey returns the metadata value assigned to key.
The job of common.m4 is to change the quotation characters (because
otherwise we will get into trouble with ' inside metadata text) and set
some default values:
[[changequote([[,]])dnl]]
[[define([[M_root]], [[/~object]])dnl]]
[[define([[M_template]], default)dnl]]
(By the way, you can probably also guess, that in writing this byte, I had to quote a bunch of code with an additional set of [[ [[ and ]] ]] to prevent M4 from replacing the names of constants or (even worse) evaluating includes—an unquoted version of this document actually fails to build due to excessive recursion.)
An important line in the script above is
echo "[[include(M_template.m4)dnl]]" >>$macrofile
This will later cause M4 to include the file given in the [[M_template]]
constant.
Note also that we don’t specify the partial HTML output file name, but instead write to stdout—it’s the makefile’s job to specify the filename for that (no particular reason for this though). We do this in the rule that calls the script
%.part.htm: %.md
$(MMD) $< >$@
Where the $(MMD) variable contains the name of the script.
So now we have two files: For some source file foo.md we have created a
partial HTML file foo.part.htm and a file called foo.part.m4. The
final output file is created by running this last file through M4. The
file defines all the macro constants that need to be replaced in the
HTML, except for [[M_body]] (also for no particular reason). It also
contains an [[include(M_template.m4)]] directive which … includes the
template. We now only need to run M4 on foo.part.m4 and pass it the body
constant set to foo.part.htm. The rule that does this is:
%.htm: %.part.htm
$(M4) -DM_body=$< -DM_url="$@" -DM_file="$(*F)" -DM_dir="$(*D)" $(<:.part.htm=.part.m4) >$@
We also pass in some constants that are derived from the target name, but that’s not so interesting.
Some pages need to be treated differently than those based on simple markdown documents. Specifically index pages that list the contents of a directory take their content not from markdown source but need to be filled in procedurally.
This is easily done by creating scripts for these pages that differ from the normal workflow. The way I do it, is by having these scripts include all the page’s metadata and page content:
# bytes.sh
cat <<EOF >bytes.part.m4
[[include(common.m4)dnl]]
[[define([[M_template]], [[figure]])dnl]]
[[define([[M_title]], [[bytes]])dnl]]
[[define([[M_feed]], [[bytes.xml]])dnl]]
[[define([[M_figure_src]], [[pie.jpg]])dnl]]
[[define([[M_figure_alt]], [[A half eaten pie.]])dnl]]
[[define([[M_figure_cap]], [[Fig. 3: Take a byte out of this delicious pie!]])dnl]]
[[include(M_template.m4)dnl]]
EOF
cat <<-EOF
<p>Here I collect some short quickfix titbits that contain very specific
technical problems and their solutions.</p>
EOF
This script could look up the directory contents itself, but I chose to hand them over as arguments from the Makefile. We can then iterate over these arguments and create list items for them: :
for post in $@ ; do
cat <<-EOF
<li><a href="${post%.md}.htm">$(lowdown -Xtitle "$post")</a></li>
EOF
done
The script is given the paths to the source files so we need to rename
them for the link target (${post%.md}.htm) and use the path to
extract the title metadatum. Now we just need to wrap this in <ul>
tags and that’s it.
For the index pages on my site, I want the entries to be sorted by date
so instead of iterating directly over the source documents, I extract
their date field, pipe them through sort and assign that to a
variable that I will then loop over:
posts=$(echo $@ |xargs -d' ' -I_file_ sh -c 'printf "%s %s\n" $(lowdown -Xdate _file_) _file_' |sort -rn |cut -d' ' -f2)
That might look a little complicated: We use xargs to pass the paths
one by one to a subshell command that writes the original value (the
filepath in this case) and the output of another subshell that extracts
the date key; after that we sort by the date field (which is the first
field in the output) and then remove that field again, because we only
want to store the paths. (I encountered this issue multiple times now
in different situations, where I want to make tuples from a single
value, sort or search the tuples, and then return the original value(s).
But I don’t know of any easier way to do this than the one I use
here…) (Note also that this incantation doesn’t work if the paths
contain spaces).
In order to make the index page use this script, we have to define a rule for it in our Makefile:
bytes.part.htm: bytes.sh $(wildcard bytes/*.md)
$(SH) $< $(filter-out $<,$^) >$@
The final HTML is assembled by the same process as any other page so we don’t need a special rule for that.
This all might seem like it’s overly complicated, but I don’t think it
is. We could do some things differently, if we wanted to: We could set
the [[M_body]] key already inside the script (if the script knows about
the macro file name foo.part.m4, it might as well know about the
partial file name foo.part.htm). Or we could not even write the
partial content into a file but directly into the macro definition,
something like:
echo "[[define([[M_body]], [[$(lowdown $mdfile)]])dnl]]"
Would that be better? I don’t know; to me it seems like it doesn’t matter.
What I like about using Make to build my website is that it can (if our setup is done right) precisely track file dependencies so that it updates those document’s whose source and template files changed, and only those! (We didn’t actually specify dependencies on templates just yet, because it’s not that interesting, but I’ll mention how to do it down below).
I also really appreciate the flexibility to be able to
give every document its own template if necessary, and to use arbitrary
metadata key-value pairs: As long as the source file contains the
value for some key, the template can use it; apart from the template
key there are no baked in assumptions about available metadata in the
script or the makefile.
The setup strikes a decent balance (for what I need it to do anyway) of flexibility and configuration. But of course it’s not perfect…
The ways in which my setup is restrictive relate mainly to paths and templates.
If you’d want to have a different mapping from input- to output
filenames that could pose some challenges. The difficult part is not
writing to different files or adding a mv to some Make rule, but that
relative paths inside the content would not get magically adjusted.
This can be worked around by using absolute paths and having some common
prefix, but that is less flexible.
Templates can only include relatively dumb text replacements and includes. There is no way to have templates nested into other templates, or inherit properties from other templates. This is a pretty static setup where other static site generators offer much more flexibility when it comes to defining templates.
It was my goal to not build my own dedicated tool, but instead just string together a bunch of standard software. Overall I’m relatively happy with this whole shebang because it is fast, sufficiently flexible, and simple. It does exactly what I need it to do and nothing more.
Something I omitted from the code I showed above is how we can make sure that HTML documents are regenerated when their template changes. We do this by having this little snippet in the shell script we run on each Markdown file:
cat <<EOF > $mdfile.d
${mdfile%.md}.html: $(lowdown -Xtemplate $mdfile).m4
EOF
This writes the target and dependency in a way that Make understands.
Then we just include the .d file in our makefile to make it aware of
this dependency that the HTML file has on the template:
-include $(wildcard *.d)
and voila: Whenever we update a template file, Make rebuilds all documents that used that template.