How Not to Make a Website
It’s 2025. How hard can it be to make a personal website?
I had the following requirements, in priority order:
- well maintained
- static website
- blog posts and pages
- good typography, including sidenotes
- extensible (without much pain)
- reader comments
- backlinks
- org mode authoring
Most of the criteria speak for themselves. For good typography, paradigm cases include gwern.net and Butterick’s Practical Typography. For extensibility, we have Emacs. And for how the web could have worked, we have the visions of Ted Nelson.
The obvious choices, upon speaking with the friendly neighborhood LLM, are Hugo and Jekyll. But I’m special damnit, I want to do things my way. So down we go into the rabbit hole of niche static website generators.
Hakyll (Haskell)
Inspired by Gwern, I started with Hakyll. Let’s look at the requirements:
| Requirement: | Status |
|---|---|
| well maintained: | good enough |
| static website: | yes |
| blog posts and pages: | yes |
| good typography: | not native, but doable |
| extensible (without much pain): | excellent |
| reader comments: | not native, but doable |
| backlinks: | not native, but doable |
| org mode authoring: | pretty easy via pandoc (a Haskell project) |
In theory, this was the highest ranking choice. I was quite looking forward to Hakyll. Having spent time as a professional OCaml programmer, and being a math nerd, I was excited to learn Haskell while I made my website. This did not go as planned. Hours were lost fighting with the Haskell tooling, which is not ready for prime time whatsoever. Integration is the hardest part of implementing any system, and solving this prolem is what it means for a product to work “out of the box”. Haskell, in trying to move as far as possible from worse is better, has seemingly failed to prioritize the naive developer experience, and I simply could not get the haddock to hook up to the Haskell language server for inspection and documentation in eglot or lsp-modeI could have fired up VS Code like a rational person, though I think this was a language server or package manager configuration issue, and that VS Code wouldn’t have been able to get the docs from the language server either. I don’t know. . This renders working in Haskell unacceptably inefficient, as one needs to constantly look up type definitions to write any code of note. And speaking of developer experience, there’s this:
Some features described in this manual are not implemented. If you need them, please give us a shout and we'll prioritize accordingly.
So…am I supposed to guess where the documentation is wrong? What’s the point of the documentation, then? I hypothesize that the practical importance of these matters, and indeed their importance above “correctness” for Haskell to be useful, is partly why the language hasn’t seen more commercial use.
What’s there to learn here? I could have included developer experience in the requirements, though honestly I expected the developer experience with Haskell to be good. If anything, the lesson is that ease of integration correlates with adoption, so expect smaller projects to be hard to use. Though I never had this problem with Clojure, Common Lisp, Ocaml, ratpoison, and many other niche softwares. Oh well.
Cryogen (Clojure)
With Haskell/Hakyll out of the running, my next thought was cryogen, a Clojure project. Requirements time:
| Requirement: | Status |
|---|---|
| well maintained: | not great |
| static website: | yes |
| blog posts and pages: | yes |
| good typography: | not native, but doable |
| extensible (without much pain): | no |
| reader comments: | no |
| backlinks: | no |
| org mode authoring: | no |
What cryogen suffers from is, in my opinion, insufficient design and poor architecture. In my experience, this is tragically natural consequence of the REPL-driven bottom-up programming approach which is really feasible in LISPs. But bottom-up is only a good idea for exploratory programming, when you are prototyping what the thing can even be. In the words of Alan Perlis: Everything should be built top-down, except the first timePerlis:15.
Take a look at Cryogen’s code structure. The first thing you notice is the flat structure. Not great, not terrible. But some of the core design choices are bad. For example, you have to choose between markdown and asciidoc, you can’t use both at the same time; there’s no logical reason for this, both are simply compilation pipelines from markup document to html. Also, there’s no built-in plugin system; you have to override the core compiler namespace to do anything usefull, which for a Lisp is a swing and a miss. Cryogen saddens me, as Clojure is my favored screw-around programming environment, but to get what I wantThe primary example being cohabitation of markdown and org-mode. Also, backlinks., I would be cloning the entire codebaseThe code base of Cryogen is, admittedly, small. and rewriting core functionality from scratch. That level of effort is out of scope. F for respects.
Nikola (Python)
Alright, two main contenders down. Who’s next? Python isn’t a bad choice, and I spent some time with Nikola. Let’s look at those requirements.
| Requirement: | Status |
|---|---|
| well maintained: | yes |
| static website: | yes |
| blog posts and pages: | yes |
| good typography: | not many themes, but doable |
| extensible (without much pain): | yes |
| reader comments: | yes, via plugins |
| backlinks: | no, but (maybe) doable via plugin |
| org mode authoring: | yes, via plugin |
I think Nikola is a good system. It has a well-thought-out modular architecture, with most of the core functionality contained in plugins. The user can write their own pluginsIn python, of course., supplementing or replacing almost anything about the program. So why not choose Nikola? Because, as far as I can tell, there’s nothing it can do that Jekyll can’t do better. We’re looking at Jekyll at ~50K Github stars versus Nikola’s ~2K, and yes this metric isn’t good, but also maybe it is good. Regardless, the question of Nikola is: are you willing to learn some Ruby if you want to build an extension? If the answer is yesWe live in the age of LLMs, so there is a right answer., then you might as well switch to Jekyll. Though maybe you stop at Hugo first.
Hugo (Go)
Hugo appears to be the dominant player in the space, with ~84K Github stars, beating out even Jekyll by a good margin. Let’s see how it does.
| Requirement: | Status |
|---|---|
| well maintained: | very yes |
| static website: | yes |
| blog posts and pages: | yes |
| good typography: | many themes, but most are crap |
| extensible (without much pain): | I guess |
| reader comments: | yes, via plugins |
| backlinks: | yes, via plugin |
| org mode authoring: | yes, but I have complaints |
Hugo seems to do some things right. It claims to render from markup very fast, and I don’t doubt itI do doubt the importance of rapid builds when incremental builds are the norm, and who is building these static websites with thousands of pages anyway?. It is well maintained, and has many plugins and themes to choose from. I didn’t bother to look into the architecture due to the existence of the plugin system (called “modules” in Hugo), which seems robust.
Where does Hugo fall short? Well, Hugo is written in Go. I don’t have a problem in this per se, but why does Hugo even exist? Go programmers seemCaveat: what do I know. to have this belief that everything should be rewritten in Go, and Hugo itself, as far as I can tell, is a good example. Another example is Hugo’s go-org library, which is a custom written org compiler (to html). Why on Earth is this a thing? To recall the situation: org itself isn’t specified by a formal grammar (already a problem for parsing), and the de facto specification is the Emacs org parser, which requires all of Emacs to run. Either you can run Emacs in batch mode to process org files, or you an (as I see some bloggers doing) exporting their org to markdown/html interactively, or you can use Pandoc. Why have a third option in Go?
Maybe Hugo exist because people wanted a new Jekyll. Here’s what the LLM says about Hugo’s advantages:
| Feature | Hugo | Jekyll |
|---|---|---|
| Build speed | 1–2 sec (1000 pages) | 30–60+ sec |
| Image processing | Built-in | Gems required |
| Multilingual | Native | Plugins (not on GitHub Pages) |
| Custom outputs (JSON or AMP) | Yes | Plugins only |
| Shortcodes with params/blocks | Yes | No |
| No Ruby dependency | Single binary | Full Ruby env |
| GitHub Pages support | HTML output only | Full (but limited plugins) |
Most of this doesn’t matter to me; Jekyll seems to do most of it with plugins, and the big disadvantage seems to be shortcodes with parmaters or blocks, which may turn out to be a big deal, it’s unclear to me. (I think the LLM might be wrong about; investigating is a TODO). What it seem to all come down to is: do you want to Ruby, or do you want to Go? Personally, I don’t see how Go is the right tool for basically scripting a templating engine. So I ended up with Jekyll.
Jekyll (Ruby)
I walked around the Earth, and I ended in the beginning. Sometimes the obvious choice is the right choice, but I will never choose it. It’s the folly of man.
| Requirement: | Status |
|---|---|
| well maintained: | yes |
| static website: | yes |
| blog posts and pages: | yes |
| good typography: | yes (imo) |
| extensible (without much pain): | yes |
| reader comments: | yes, via plugins |
| backlinks: | yes, via plugin |
| org mode authoring: | yes, via plugin (uses Emacs in batch mode) |
It was easy to get Jekyll working, I’m not going to bore you with that. The real choice was to the theme. Some basic searching/LLMing lead me to tufte-jekyll, which seemed to do the job. I vendoredI.e. I cloned it. it into my repository, and then—woe—it didn’t build. Something about outdated function signatures in some library, I didn’t want to dig. So onto the next one, jekyll-theme-tufte. As far as I could tell, jekyll-theme-tufte mainly exists to bundle tufte-jekyll as a (Ruby) “gem theme”, allowing directory separation of the theme assets/logic from your content. Personally, I have the opposite philosophy; let the code and the content live in the same repo, second-order ontologies be damned. So I vendored jekyll-theme-tufte in, built it (it worked), deleted the existing demo content, tweaked a few things, and here we are! Exhausted, but victorious. For now.
How to end this off? Unclear. What did I learn? I suppose that, the question is always “build or buy”, and don’t build unless you have to. I will miss you, Haskell. So it goes in life.