A static generator without dependencies
This website is built by a single Python script. No Hugo, no Jekyll, no Eleventy, no framework, no pip install. Just build.py, importing the Python standard library and nothing else. If that sounds odd to you, I get it. There are mature generators with thousands of users, themes, plugins. Why build your own?
The answer is not "because I can". It's because I want to understand what's happening.
The starting point
When I set up the site, I looked at what a static generator would actually have to do for me: read Markdown, convert it to HTML, put it in a template, write files out. That is not rocket science – it's text processing. And text processing in Python is exactly what the standard library does best.
I had two options:
- Install a generator, learn its configuration language, its directory structure, its theme system – and then operate a black box I can only debug through the documentation at every problem.
- Write the four things I actually need myself – and then know exactly what every line does, because I wrote it.
For a site with a few pages and a blog, the second option isn't bold, it's simply the smaller maintenance burden. The generator is now 850 lines, and every one of them I wrote with a concrete requirement, not inherited from a framework default.
What the script actually does
The core is manageable. A parse_page() reads front matter and body text from a Markdown file. An md_to_html() converts the text – deliberately a small subset of Markdown: headings, lists, quotes, code blocks, bold, italic, links, images. I don't need more, and every unsupported syntax is a feature: it can't break either.
The template is a single base.html with placeholders like {{title}}, {{content}}, HomeConsultingBlogShop. The build replaces the placeholders and writes the finished pages to public/. public/ is completely deleted and regenerated on every run – there is no state that lingers.
Two things I deliberately built in, because they had hurt me before in other projects:
Bilingualism as first class. Every post exists in content/de/blog/ and content/en/blog/. The alt: field in the front matter links the language versions to each other, hreflang tags tell search engines which version is for which language. That's not a plugin, it's ten lines in render().
Cache busting via content hash. style.css sits in the browser cache with max-age=604800. If I change the CSS, visitors still load the old file for up to seven days – new HTML against old CSS. The solution: the build computes the SHA-256 hash of the file content and writes the file as style.<hash>.css. Same content, same filename, no unnecessary cache break. That's the mistake that previously wrecked a contact form for me – now it's caught in the build itself.
What I did not build myself
A generator that only knows my use case is brutally honest in one place: it can't do anything I didn't explicitly build. And that's exactly the point. I didn't build a generic solution, I built my solution. Which means:
- No tables in Markdown. The parser can't do them, so I don't write them – and on a smartphone, prose reads better anyway.
- No theme ecosystem to get lost in. The layout is one HTML file I fully understand.
- No dependency that stops being maintained in three years and breaks my build.
build.pyruns as long as Python 3 runs.
The trade-off
I wouldn't advise anyone to write a generator themselves for a large project. But for a site with a blog, two languages, and a handful of pages, the calculation is different: the learning curve for a foreign framework is higher than for the four functions I actually need. And the price of the self-built solution – "I have to do everything myself" – is here also the gain: I know what every line does.
The honest core message is the same as with the backups I wrote about last week: the best generator isn't the one with the most features, it's the one you actually run and where you don't have to guess why something is broken.
The source code is the documentation. It's 850 lines, no dependencies, and I can explain every one of them.