How this site is built

2020-08-21 08:51:10

Obviously most sites in 2020 are enourmous beasts, running megabytes of javascript on both the client and server. This one isn’t, and I thought I’d take some time to explain how it does work. I want to write an article critical of the web as a whole at some point so I haven’t put any reasoning for why it works this way in this post.

This is no ordinary site

This is both a blog and a phlog. All of the posts are available on both a website and a gopher server, both at If you haven’t heard of gopher, go have a look! I hadn’t until recently but it is a lot of fun. I got started here.

Server Side: HTTP

There is basically nothing on the http server. It is a rust executable using actix that serves files from a directory. All the pages on this site are just html files. The only interesting thing it does is redirect / to /index.html.

Server Side: Gopher

Even simpler, I didn’t need a gopher server library since the protocol is wonderfully simple. Again a rust executable is serving static files, and / serves a file called index.

Server Hosting

These are hosted on a debian buster server with vultr. I wrote a couple of systemd unit files for my executables and they run 24/7 and autorestart should they fail.

Client Side

The phlog (gopher log) just serves the raw markdown files that I write, but the blog has some additional styling. A tiny bit of css which uses css-grid to layout the pages and make them responsive, as well as gruvbox colours.

Directory structure

The project root has:

Build process

I’ve hinted that I write markdown posts but obviously they are published as html. To generate all the pages for the site I’ve used a bunch of python scripts connected together with tup, which I hadn’t used before but I quite like now. So far there are 4 python scripts:

These are all pretty simple but html_src/ is the most complex and interesting so I’ll break that down briefly.

import argparse, json, sys, os, subprocess, shutil

parser = argparse.ArgumentParser(description="Generate html for post from markdown")
parser.add_argument("post", help="Post markdown file name")
args = parser.parse_args()

with open(os.path.join("posts",".md"), encoding="utf-8") as post_file, open("config.json", encoding="utf-8") as config_file, open("html_src/post_template.html", encoding="utf-8") as template_file, open("html_src/nav.html", encoding="utf-8") as nav_file:
    config = json.load(config_file)
    post_metadata = next((p for p in config["posts"] if p["file"] ==, None)
    if not post_metadata:
        print("Post not found")
    for line in template_file:
        if line == "{content}\n":
  ["markdown"], stdin=post_file, stdout=sys.stdout)
        elif line == "{header}\n":
            """.format(title=post_metadata["title"], date=post_metadata["date"]))
        elif line == "{nav}\n":
            shutil.copyfileobj(nav_file, sys.stdout)

This isn’t a python tutorial so I’ll be quick. It takes an argument with the name of the post (not including the extension). It finds the metadata for the post in the json file, then reads the template line by line looking for substitution points which is substitutes accordingly.

The Tupfile that runs all of these scripts is:

: foreach html_src/*.css |> cp %f %o |> html/%B.css
: |> ./html_src/ > %o |> html_src/nav.html
: foreach posts/*.md | html_src/nav.html |> ./html_src/ %B > %o |> html/%B.html
: foreach posts/*.md |> cp %f %o |> gopher/%b
: | html_src/nav.html |> ./html_src/ > %o |> html/index.html
: |> ./gopher_src/ > %o |> gopher/index

Finally I have a that runs tup and then rsync to copy all the updates to the server.

The git repo with all this stuff in can be accessed using git clone git://


I was pretty pleased with myself once I got this working so thought I’d write this, but it’ll probably improve further. Maybe I’ll make this a series. As always my email is