Make the Web

I recently stumbled upon The Lost Art of the Makefile. It was a timely find, in that I too had been circling back to make and the article expresses some of my opinions. It’s a tool that is not only still relevant, but, I argue, more enjoyable and apt for many scenarios than the modern tools that get chosen by default or vice.

Shortly before finding the article, I’d taken a little Sunday excursion to write yet another static website generator. It was intended to be notoriously simple, not just technically, but conceptually; perhaps reacting to a perceived cognitive bloat of today’s Web, I’d decided that a generated website could be nothing more than an index and individual article pages. Articles should feature no automated page-to-page navigation, no metadata. Initially, even the style was kept to a single line constraining the maximum width of the page — it was then refined1, but only minimally.

The result is a twenty-eight-line Makefile. I won’t break it down, since the aforementioned article is a good introduction to the subject. That said, a few highlights of the unimaginatively named makeweb are in order.

Jump to the coda if you’d like to see the site generated by the Makefile.


Deep dependency awareness

The terseness of the target-based rules is something that I’ve missed from my University years, where Makefiles were commonplace, along with other traditional development practices. There is nothing surprising about this system if not for how much make provides out of the box without language-specific knowledge. The reader is invited to modify different files of the project and run make to see what gets rebuilt. Succinctly:

File edited Files regenerated
pages/ public/article-1.html, public/index.html
pages/article-2.html public/article-2.html, public/index.html
templates/header.html public/*.html
templates/index-begin.html public/index.html

make tracks changes in the dependency chain of each artefact in order to only run the targets (i.e. recompile) what is needed. This means that building a large site, even perhaps a deeply nested one with nested Makefiles, can be very performant. The setup–payoff ratio is refreshingly high, in an age of convoluted NPM-webpack-foobar build systems.

Multiple source formats

Despite its size, the makeweb Makefile can accommodate both HTML and Markdown as content sources. More interestingly, dependency awareness isn’t lost, thanks to the concept of optional dependencies2, i.e. if attempting to build public/a.html, the tool knows to look for pages/a.html or pages/ and check their modification dates to avoid unnecessary recompilation, but it doesn’t need both source files. Since Markdown allows some HTML formatting, this accommodation is probably moot from a product standpoint, but the technical question stood nevertheless — different source formats could trivially be adopted later. I left this feature there for the sake of example, perhaps hypocritically, as it goes against the premise of simplicity of this exercise.

Simple convention over sophistication

One might be tempted to introduce the notion of page metadata (title, date, author, locale…) and dedicated parsing of said data. That was my choice in a larger site-generator project in 2009, in which source files were Markdown-powered YAML:

- date:     2009-06-28
      locale        : 'en_US.UTF-8'
      date_format   : '%A, %B %d, %Y'
  keyword:  sample test
  title:    Test Page
  body: |

      This is a sample article. It'd be nice if it could be recognized as _Markdown_. […]

The idea had merit, but why bother? What is really needed, anyway? Thus, a page is expected to sport a h1 tag containing its title. Similarly, its filename is expected to start with a date in YYYY-MM-DD format3, currently only used for sorting the index. Finally, locale can be expressed via the lang HTML attribute, whether globally (in templates/header.html) or on a per-page basis, and author and other such data could be provided with meta elements.

These conventions allow things like title extraction for the index to be performed trivially as a shell operation inside the Makefile:

for p in $(subst public/,,$(filter-out templates/%,$^)) ; do \
  t=$$(grep -m2 '<h1>' public/$$p | tail -n1 \
    | awk 'match($$0,">[^<]+<"){print substr($$0,RSTART+1,RLENGTH-2)}') ; \
  echo "<li><a href="$$p">$$t</a></li>" >> $@ ; \

On correctness. The makeweb Makefile presents concessions made in the name of brevity. For instance: strictly speaking, targets all and clean should be declared .PHONY targets, since running those targets doesn’t generate files by those two names. However, that omission should never be an issue given the intended use of the Makefile and given implicit conventions. The .PHONY declaration takes just one extra line, so cost of implementation is not an issue. Instead, the point here was to highlight that a different mindset is allowed when we treat the software we write as a minimal tool that will be used in its entirety — as opposed to via opaque interfaces — by someone else. Similarly, the code that looks for a page’s title is naïve, but the point is that the user of the tool will be working with the tool, and that we don’t need to guard against every hypothetical failure. This can be very liberating.

No configuration as a feature

Allowing no configuration or extension mechanisms encourages the Makefile to stay small. Indeed, at this scale, tools can be readily understood, and thus forked and specifically tweaked for each usage. In this scenario, config files and plugin systems are hard to justify: if one needs an automated extension solution, systematic patch application is an apt solution. Consider the following example:

Suppose a different Markdown processor — or a different set of processing rules — is desired for some reason. Rather than having the Makefile proper offer that option, which could look like:

# Offer a default processor
MARKDOWN ?= python3 -m markdown

    cat templates/header.html > tmpfile
    $(MARKDOWN) $> tmpfile
    cat templates/footer.html > tmpfile

one instead keeps a patch:

diff --git a/Makefile b/Makefile
index 7509045..d96cf95 100644
--- a/Makefile
+++ b/Makefile
@@ -18,7 +18,7 @@ public/index.html: $(DST_PAGES) templates/*
 public/%.html: $$(wildcard pages/$$*.html) $$(wildcard pages/$$*.md) templates/header.html templates/footer.html
    @echo $@
    @if [[ '$(findstring .md,$<)' ]]; then \
-       python3 -m markdown $ tmpfile ; \
+       my-markdown-processor $ tmpfile ; \
    else \
        cat $ tmpfile ; \

First off, why not offer MARKDOWN as an environment variable? It’s a reasonable request. But the underlying question is: where should the line be drawn? Should custom flags for the Markdown processor also be configurable? Should control flow within the Makefile also be configurable for user-defined error handling?

Instead, this patch, along with other possible patches, can be collected and reapplied to the original Makefile at any time. If this sounds farfetched, know that it can be a very simple and pleasant workflow, with the right tooling (perhaps make itself). The software school of uses this pattern. I can say it was a joy to customize dwm with community patches and my own settings and have this process handled by Arch Linux’s excellent makepkg tool.



All these little tools may seem intimidating at first, with their cryptic names and options, when compared to ready-made alternatives. This paragraph is a short éloge of the small, the simple and the composable. When reading about make, grep, cut, awk, bash, one may come across the terms GNU toolchain or the UNIX way. Behind these terms lies the idea that, rather than gravitating towards specific tooling ecosystems depending on one’s chosen technology (should technology matter, or shouldn’t indeed the nature of a problem be more determinant?), one should find specialized tools and techniques that recognize and celebrate the universality of text and files. For this reason, shell scripting and deep editing skills — proficiency in Vi(m), Emacs, or comparable — are as relevant today as thirty years ago. Finally, once acquired, these skills will be useful throughout an individual’s computing life.

Parva sed apta

Screen Shot 2018-08-26 at 15.48.45
A website generated with makeweb can be visited at It does, shows and weighs less; what remains is the content — the value of its substance is a different topic. That website isn’t meant to replace my actual one, but it may shape it.


As Gutenberg — a project orders of magnitude more complex than any Markdown-powered site generator — is reaching merge proposal state for inclusion in core WordPress, these pseudo-philosophical detours are an opportunity to remember what the ultimate goals for WordPress are: writing, publishing, building one’s home on the Web.

Gutenberg semantically augments the content so as to help writers write and readers read; it absorbs disparate concepts of WordPress content and presentation (shortcodes, widgets, menus, embeds) into a single concept of the block; and, while any tool can be misused, there is potential in the block paradigm to reclaim content as the central entity of the Web, and to simplify our little virtual homes.

  1. Despite my questionable taste. 
  2. See usage of .SECONDEXPANSION and wildcard in the source. 
  3. Let’s face it: it’s the only sane format. We don’t tolerate that MM-DD-YYYY nonsense here. 

Author: Miguel Fonseca

Engineer at Automattic. Linguist, cyclist, Lindy Hopper, tree climber, and headbanger.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: