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.

Static Site Generators

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 website

The 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.

Source and Template files

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?

Partial HTML and M4 directives

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.

Running M4 to generate final output

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.

Index pages

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.

Evaluation

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.

Nice

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…

Meh

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.


Bonus Round

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.