Your blog doesn't need to scale

October 31, 2025

I'm rewriting my blog for the nth time. I think the reason why I, like many other developers, fall into this loop is that it's fun to build the tool, and is a distraction from actually writing. That's to say, I'm fully aware that this is not productive from the perspective of a writer.

My last iteration was a foray into a more "practical" approach, building on a proven, existing framework - Hugo. The idea was that this would let me focus less on the technology and more on the writing. But... I like the technology, and removing that aspect was depriving myself of one of the main reasons for having a blog: it's a place to experiment with weird new ways of doing things. Using Hugo felt like a chore because I needed to learn the patterns that someone else thought of.

A blog doesn't need to be webscale. It's not my day job. It's cool that I can make it work exactly the way I want it to work, because it means I get to choose the set of tradeoffs that I find suitable or interesting. It's also just more fun, and that's the point.

Tradeoffs

I think when you're learning programming, you naturally seek out the perfect approach: a hammer that you can learn to use and hit every nail with it. This is why people will forever argue about which framework is best. In any pursuit, not just programming, people want to feel like they're doing things the right way. Because otherwise, they're wasting their time learning something suboptimal. I'm certainly guilty of this myself, at least. I'll do an excessive amount of research in search of the optimal way to learn, when the reality is you can't know what is going to work best for you until you try.

Coming back to software, the longer I practice, the less I can confidently make a claim that X is better than Y, full stop. Or that Z is bad, full stop. For example, I used to think Go had fundamentally suboptimal design - no generics, and ridiculously verbose error handling. The first point might have had some merit, as Go 1.18 introduced generics in 2022. However, working with Go in some microservices, I've come to appreciate the readability, and learnability of Go. There's no magic, every curtain can be pulled back to reveal Oz. This is a tradeoff I've come to value, since it means you can really understand how something works if you want. Or more importantly, when something breaks.

This stands in stark contrast to my early programming language love, Haskell. There is a distinct feeling of mysticism surrounding some of the core abstractions: applicatives, monads, IO, state, and many more. To me, it felt like there must be some wisdom to be gleaned from studying them; otherwise, why were they so hard to learn? Maybe I wasn't wrong, in the sense that abstract math is worth studying on its own merit, but after 5 years of experience in the industry, I can confidently assert that my time meditating on a -> (a -> m b) -> m b was not a fruitful effort for the purpose of becoming a better developer.

That's not to say Haskell can't be practical, just that its tradeoffs don't seem that useful for the kind of work I do. I haven't seen it in production, but it seems like people are concerned with practical usage, based on movements like Simple Haskell.

In the context of blogs, I think you have to decide what criteria you're trying to optimize. Here's some I identified as I was thinking about my ideal architecture for a blog.

Performance

This is obviously important in any software, but it's interesting for blogs, because the dumbest possible way to do it - writing HTML by hand - is about the fastest you can get (barring minifying your assets). You'll only get into noticeable trouble if you dynamically render content on the client.

Compatibility

The modern web platform is great. The old web... sucked. I haven't had a ton of exposure, but I've dealt with enough IE11 flexbox and other CSS shenanigans that I have no interest in supporting old browsers. I don't need you to be using bleeding edge nightly Chrome builds, but if your browser doesn't support features that MDN considers widely available, what are you even doing? I think the more you try to optimize for this, the more concessions you'll need to make in ease or simplicity.

Ease

How easy is it to get up and running? This is what pre-built frameworks will generally give you. Even if you have little programming experience, you could probably stand up a Next.js or Hugo blog in a couple hours.

Simplicity

How easy is it to understand how your app works? This is not the same as ease; if you understand how to work with your framework's abstractions, but you don't understand how those abstractions work, you don't understand how your app works. There's subjectivity here - you need to decide how low-level your comprehension needs to go. For me, I'm willing to overlook the implementation details of web standards and networking. Once your browser has fetched my assets, the ball is in your browser's court.

Maintainability

If you take a break from your blog and come back to it after some time, will it still build? In 6 months? 2 years? 10 years? On a new machine? New operating system?

Fun

This is the most subjective, but also the most important. Do you like the technology you're using, or is it a drag? This isn't a good criterion for something you're trying to turn into a product, but we're doing this for fun, not to make money. If it isn't fun, you're going to abandon it.


You can't have it all. If you want to support old browsers, you're either going to need to need polyfills or you'll need to avoid using new web features. The easier you want it to build on top of your framework, the more abstraction you need, and the less simple it becomes.

I don't know if maintainability ≡ simplicity, but it's obvious that more moving parts in the infrastructure means more moving parts that can break. For this iteration of my blog, maintainability is going to be my main focus. I don't want to come back to my site and not be able to build it because one of my dependencies has reached end of life.

HTML is Enough

Markdown is the king of markup languages because it's easy to read, even from the plaintext representation. It's a natural format for internal documentation. It makes sense then that we, as programmers, would turn to it for writing blogs.

It does make a few things dead simple, but let's compare with writing raw HTML:

Paragraphs

Instead of Para, write <p>Para</p>

Headings

Instead of # Heading, write <h1>Heading</h1>

Links

Instead of [Label](url) write <a> href="url"</a>.

Admittedly, Markdown does have the alternate reference style links, where you put all the links in one place, and reference them by number. This cuts down on the visual noise when you're reading the raw Markdown.

Bold/italics/underline

Instead of **bolded** write <strong>bolded<strong>.

Instead of _emphasized_ write <em>emphasized<em>.

Instead of __bolded__ write <u>bolded<u>.

Code

Instead of `code`, write <code>code<code>.

Instead of:

```
line 1
line 2
```
Write:
<pre><code>
line 1
line 2
<code><pre>

This is admittedly pretty annoying when you're writing HTML because you need to escape HTML-encoded characters. This is what I actually had to write to get the above to render:

<pre><code>&lt;pre&gt;&lt;code&gt;
line 1
line 2
&lt;code&gt;&lt;pre&gt;</code></pre>

Of course, to write that, I had to add an additional layer of escapes (this kinda reminds me of that guy on Reddit taking pictures of how he took pictures of himself).

Links

The other benefit is that you can centralize the translation from Markdown to HTML. If you decide that an element needs a different class, you only need to update one place rather than N. On the other hand, for a blog, IMO you should use semantic HTML and style them with CSS, like above.


We like Markdown because it's a simple abstraction, but it's not that useful, and it's not free. The web speaks HTML, not Markdown, so you'll need to translate it at some point. That means either adding a dependency or writing a bunch of code. It's also just another layer of abstraction you need to keep track of. In my opinion, the utility of the abstraction doesn't pay for its weight.

Like most things, there's a time and place. The scale might tip more in Markdown's favor if you're outputting in formats other than HTML, or if you're collaborating with non-programmers.

Reusing HTML

I think it's pretty strange that there isn't a standard way to reuse HTML content on the client via includes. There's a solution for reusing JS and CSS, but not HTML. Chris Coyier wrote a great article about this. The upshot is that we have three options:

Rendering on the client seems like shooting myself in the foot performance-wise, as I described above in the tradeoffs section. So my options are either introduce a build step or just duplicate everything. As you may have gathered from my stance on Markdown, I'm not opposed to doing things manually, but needing to touch every single post manually when I adjust the layout seems like a big waste of time. So I will use a build step to avoid that, but it will be reasonably minimal.

Dependencies

Unless you're going to write everything in assembly, software dependencies are a fact of life. Web developers have gone a little overboard with it, pulling in dependencies for trivial code. Dependencies aren't free, they're a liability you need to justify. Aside from the obvious security aspect, with supply chain attacks becoming more prevalent, dependencies are a failure point for your application, at least as they're typically used. When you npm install a package, you're putting trust in a third party to keep that code available, even if you're pinning specific versions.

Here's my full list of dependencies.

Node

Plain Javascript for backend seems to have fallen out of style, but there's no denying that Node is massively popular. That's a good thing for maintainability, because it means the project is likely to still be around for many years to come. Python was another viable choice here, but it is convenient to only need one language toolchain between frontend and backend. Node and browser Javascript aren't identical, but they have become very close.

pnpm

Given Node, there's only two reasonable choices for package manager: npm or pnpm. npm is still the most popular of the three package managers, with yarn taking third place. I don't see any compelling benefits of yarn over pnpm, so I'm disqualifying it. npm is the safest choice, but I'm choosing pnpm because it seems to have better support for vendored packages, and it's mostly a drop-in replacement for npm. It's also much faster. In a pinch, it should be easy to fall back to npm by packing my dependencies into .tgz and installing them as file: dependencies.

Eta

Eta is an HTML template engine written in Javascript. Great template engine support was another must-have for my choice of programming language. I considered a number of alternatives:

Preact

Preact is a lightweight alternative to React. I actually only have experience with Preact, not React, and I see zero reason to switch to React. I'll be using this for the interactive parts of my pages, when plain JS won't cut it. It's possible I'll switch to Solid in the future, but Preact seems to actively support zero build step use cases.

I'll also be using @preact/signals rather than Preact useState. From my experience using Solid, signals are easier to reason about than state hooks. Annoyingly, this requires me to vendor not just one, but two more packages: @preact/signals and @preact/signals-core. I have no idea why they did it this way, but the core functionality is implemented in signals-core.

htm

HTM adds JSX-like syntax using Javascript tagged template literals. This is not strictly necessary, as I could write out Preact components using just the h(tag,props,children) function that Preact exports, but that's slightly miserable to use.


I'm taking a more extreme approach by vendoring my dependencies when practical. I will choose specific versions of each to check into source control. As long as I can obtain my core programming language and package manager on a new machine, I should be able to build the site.

Here's proof that I can add interactive widgets. Drum roll please...:

Javascript is Enough

That was an exhaustive list of dependencies. I am not using any build tools for Javascript - the JS files are copied as-is to the dist folder with the rest of the assets. VSCode has strong support for JSDoc, which lets me type my scripts without using Typescript.

Typings for my dependencies requires a bit of trickery. I need to store the original source for the projects to get the typings, but I want to serve the minified dist files.

When I import a dependency like import { render, h } from "preact", my editor and the browser are both able to understand it:

Admittedly, the case against a build step isn't as strong as it once was. Vite has really streamlined the process of adding features to your app. What I like about skipping a build step is that it frees me from the paradox of choice. Since I'm constrained by the limitations of Javascript as browsers support it, I don't have to worry about whether I could make things easier for myself by adding another plugin. And let's be honest, I'm doing it because it's plain cool. Building on top of modern web standards, this static site has hardly any moving parts, and I'm not paying that much for the privilege.

Desires for the Future

I've learned something from each iteration of this blog but I've failed at actually writing. I have a number of unfinished drafts with some interesting ideas. I think I'm more willing to do a full rewrite of the blog because that work is mostly invisible on the outside. By contrast, it's scary posting your ideas for others to read and critique.