All posts

Moving The Little Hub Off Hugo

Why the site now builds with one Python script

If you're reading this, it was built by a single Python file I can read from top to bottom. Until this week The Little Hub ran on Hugo 0.72, a version from 2020. When I finally went through the old site properly, the Contact page was still showing lorem ipsum and the About page was Hugo's own boilerplate, describing Hugo. Both had been live for years.

So I replaced it: a new design, and a new generator that numbers posts the same way I number everything else.

Why Not Just Update Hugo

Hugo is good software and most people should use it. I wanted two things it doesn't do out of the box. Every post should carry an ID from the scheme I already use for work and project documents, and a published post should keep a visible record of anything that changed after it went out.

I could have bent Hugo into that with archetypes and custom front matter. I'd rather have a small tool that does exactly this and nothing else.

The Numbering

Every post gets an ID. This one is TLH-TM-1010.2:

PartMeaning
TLHThe site
TMTechnical Memorandum, a write-up
1010The subject, "Site generator"
.2Second document in that subject. The first is the generator's operations manual.

The ID is the permalink. This post lives at /posts/TLH-TM-1010.2/ and stays there even if I change the title.

Posts also follow a revision ladder borrowed from engineering drawings. A post starts as a Draft. Publishing it makes it Rev -, and any change that alters its meaning moves it to Rev A, then B, and so on, skipping letters like I and O that get confused with numbers. Typo fixes don't count. Once a post has been revised, its revision history appears at the bottom of the page.

For a blog with a dozen posts this is probably overkill. I already work this way everywhere else, though, and a post that quietly changes after people have read it is something I'd rather not do.

What It Is

One file, tlh.py, about 1,300 lines. It uses only Python's standard library, so there's nothing to install. It runs on Python 3.8 or newer and has been tested on 3.13.

Day to day it's four commands:

python3 tlh.py                           # new post, asks a few questions
python3 tlh.py serve                     # local preview, rebuilds on save
python3 tlh.py baseline TLH-TM-1010.2    # publish
python3 tlh.py build                     # render the site for upload

A registry file keeps track of every ID, so two posts can never get the same number. tlh.py check compares that registry against the actual files and complains if they disagree. Drafts only appear in the local preview, never in a real build.

The Trade-off: Markdown

The standard library has no Markdown parser. I could add one as a dependency, or write a smaller parser that covers what I actually use. I went with the smaller one: headings, lists, code blocks, tables, links, images, and raw HTML when I need something odd. It doesn't do footnotes or syntax highlighting.

If I start missing those, I'll add a proper library and write the decision down. Until then, a dependency I don't need is just one more thing to keep updated.

Hosting

The build writes plain HTML into a folder, which I move to my web server with rsync. Every build also writes a .htaccess file with three jobs:

  • Showing the site's own 404 page instead of server's default.
  • Sending the RSS feed with the right content type.
  • Redirecting old Hugo addresses to the new IDs. /posts/better-git-commits/ now goes to /posts/TLH-TM-3030.1/, and Hugo's old feed at /index.xml points at /feed.xml, so anyone subscribed doesn't lose the site.

The redirects live in the site's config file, and the build warns me if one points at a post that doesn't exist or isn't published yet.

Testing against a real Apache install caught one trap before it went live. Turning off directory listings from .htaccess makes every single page return a 500 error on servers that don't allow that particular override. The option exists, but it's off by default.

The Design

I consider myself not the best at front-end design. I can do it but I'll complain about what I come up with. To that end, I use Claude design to help create something of a scaffold.

I deliberately avoided the look most LLM-generated sites have right now: purple gradients, the Inter font, rows of three cards with icons, and the newer "tasteful" version with a cream background and terracotta accents.

What's here instead is a white page, Schibsted Grotesk for headings and Source Serif 4 for reading, and a single colour taken from a highlighter pen. The home page lists posts by year, including 2023 and 2024, when I posted nothing. I'd rather show the gap than hide it.

How It Was Built

I built this assisted with Claude. I set the constraints and made the calls: standard library only, my numbering scheme, ask before adding any library, and no AI design tells. Claude wrote the base scaffold of the code again and the operations manual. The irony of using an LLM to build a site that deliberately doesn't look LLM-made wasn't lost on me.

What's Next

Moving the older posts across. Each one gets an ID, its original publication date, and a redirect from its old address, so nothing that links here breaks.