This is the article about the website that hosts this article, and how it is assembled.
First up: Why not use Wordpress? Why not use Ghost? There are hundreds of ways to create a website. Do they all suck? Well, probably not!
But I like things to be maintainable. Long term. So when I come back to them later everything still just works. Love 'em or hate them, Makefiles are kind of a great way to leave an instruction manual behind for a small project. And so this website is really just a bunch of scantily clad HTML, JavaScript and CSS, wrapped with a tawdry Makefile.
The problems
- Placing the header and side menu on every page, without copy-pasting the same code everywhere
- Automatically keeping the front page up to date with the list of articles
- Automatically keeping the RSS feed.xml up to date with the list of articles
A solution...
I added a small compilation step that automatically runs when my text editor
performs a save. This is not miraculous, and it is dense, but it makes
me happy:
Makefile
SRC_HTML := $(shell grep class=.published src/*.html | sort -t= -k2 -r | cut -f1 -d:)
OUT_HTML := $(patsubst src/%,%,$(SRC_HTML))
SHELL := /bin/bash
.ONESHELL:
all: $(OUT_HTML) feed.xml index.html
%.html: src/%.html src/header.tpl
export MAIN="$$(cat $<)"
envsubst '$$MAIN' < src/header.tpl > $@
index.html: src/*
get_field() {
grep class=.$$1 $$2 | awk -F'[<>]' '{print $$3}'
}
export MAIN="<ul class="flex">";
for i in $(SRC_HTML); do
export HTML="$$(basename $$i)";
export TITLE="$$(get_field title $$i)"
export PUBLISHED="$$(get_field published $$i)"
export WC="$$(get_field wordcount $$i)"
export CATEGORY="$$(get_field category $$i)"
export DESC="$$(get_field desc $$i)"
export STATUS="$$(get_field status $$i)"
MAIN+="$$(envsubst < src/index-$${STATUS}.tpl)";
done;
MAIN+='</ul>'
envsubst '$$MAIN' < src/header.tpl > index.html
feed.xml: src/*
get_field() {
grep class=.$$1 $$2 | awk -F'[<>]' '{print $$3}'
}
export MAIN=""
for i in $(SRC_HTML); do
export HTML="$$(basename $$i)";
export ID="$$(basename $$i .html)";
export TITLE="$$(get_field title $$i)"
export PUBLISHED="$$(get_field published $$i)"
export DESC="$$(get_field desc $$i)"
export LONG=$$(date -d "$$PUBLISHED 09:00:00 +1100" "+%a, %d %b %Y %H:%M:%S %z")
export STATUS="$$(get_field status $$i)"
if [ "$${STATUS}" != "todo" ]; then
MAIN+="$$(envsubst < src/feed-item.tpl)";
fi;
done;
envsubst '$$MAIN' < src/feed.tpl > feed.xml
.PHONY: all
This Makefile can be invoked in three different ways:
- To build the html files for the articles
- To build the index.html home page with a summary of all the articles
- To build the RSS feed XML file
The first target basically allows you to run, eg make somefile.html where it will then look for a similar file in the src/ folder, and mix it together with the header.tpl file (that contains the side menu) and generate a finished html file.
Eg: make tiny.html uses: /src/tiny.html and /src/header.tpl to generate the entire top-level file /tiny.html that you are reading now. Better yet make all rebuilds all of the articles, the index and the RSS feed in one go.
It uses the program envsubst to swap environment variables in a file, in this case the contents of /src/tiny.html get assigned to the $MAIN variable and envsubst swaps it into src/header.tpl - a cursed template to be sure!
src/header.tpl
<html>
<head>...</head>
<body>
<header>
(side menu code)
</header>
<!-- Page content gets swapped in here -->
$MAIN
</body>
</html>
And because it's make, it only builds particular files if it really
needs to (i.e. if the file in src/ have changed). The end result is a compile
step that's faster than a blink.
The other two targets, for index.html and feed.xml are quite similar to each other. They both go through the files in the src/ folder in a loop, and extract out the information they need to generate their resulting collection pages.
Text editor tweaks
To automatically execute make all when the text editor saves, I added a
snippet to the vim configuration.
.vimrc
" update blog after a write
augroup BlogProject
autocmd!
" ensure pwd is correct for make
autocmd BufEnter /path/to/blog/* lcd /path/to/blog
" re-generate html files whenever something changes in src/ folder
autocmd BufWritePost /path/to/blog/src/* silent! make all >/dev/null | redraw!
augroup END
To conclude
This approach feels minimal and understandable. It results in fast page
loading, and the utilities involved: make, envsubst and vim are widely
available and will likely outlive us all.
However, I do concede that in writing all of this down it does feel like I'm a crazy person, but at least it is now somewhat well documented (the code, not the minutae of my sanity).