How I used boring tech to build this website.

written on 2026-04-24

When evaluating what tools and software I should use to build my blog there were a few things to consider.

  1. Simplicity
  2. Control
  3. Long term use

I landed on Jinja2. I don't really want to use something in the node ecosystem as I find that a few months later my web of dependencies often breaks. Python also has this problem... but it's generally more stable, especially if I select few and specific dependencies. If I do that with JS I'd probably pick something like Astro which has had 4 major version upgrades in 3 years. (Not trying to piss off anyone using Astro but seriously... this is such an annoying pattern with the node/js ecosystem)

I also don't need to npm install anything. I can just inline stuff into my JS files as needed per page/article. If I even need any JS dependencies. Mostly I'll end up writing JS by hand for articles if I want to make a page more interesting.

One thing to consider was that since this is a blog about development I'd probably want to include snippets of code with highlighting. So I picked a boring package (dependable, stable, old) called Pygments. This allows me to do my highlighting at build time avoiding yet another JS library.

Jinja2 and Pygments are enough for my build process. But this does leave a bit of a gap if I want a good "DX". I think developer experience is often prioritized too highly. I'm not sure that developer experience should matter so much that we end up shipping blobs of shit to our users. We should learn to use the tools before reaching for additional packages/libraries. That being said I did end up reaching for a few more lol (SHAME!). Do as I say not as I do type of thing...

I was hoping that including Pygments would be my last dependency ... but I dont want to be swapping to a terminal, calling uv run main.py --build, tabbing back to my browser, and then hitting refresh every time I want to preview an article. This fucking sucks.

First things first I needed something to call uv run main.py --build whenever I make a file change. I could do this using a python library. But I have a tool for that I use in other projects; watchexec. This is probably the most "radical" tech here. It watches a list of files/directories and can call a program. All through a CLI. This means I don't need to install anything new or add any dependencies. Instead I will just call cargo install --locked watchexec-cli and now I can use my main.py script as what python was intended for: a script.

subprocess.run([
	"watchexec", "-i", "dist/**", "-e", "js,css,html,py", "-r",
	"--stop-signal", "SIGKILL", "uv run main.py --dev"])
Running the code with --dev will build our templates and run our aiohttp server (which is introduced below).

This leaves us with one additional feature: auto-refresh for the browser. In my experience with frontend frameworks they typically just open a websocket and pass around data. Since I don't really care about "frontend state" I can just wipe it each time. I'll open a websocket and if it closes I'll refresh. This means I can't just open up index.html anymore. I'll need some kind of backend server (during development) which will serve files and listen for websocket connections. If the connection closes I can assume the backend rebuilt and the frontend should just refresh.

So I added aiohttp. I can create a "web application" and serve files via web.static and websockets with web.WebSocketResponse.

async def websocket_handler(request):
	from aiohttp import web

	ws = web.WebSocketResponse()
	await ws.prepare(request)
	async for message in ws:
		if message.type == web.WSMsgType.ERROR:
			print(f"connection closed with error: {ws.exception()}")
	return ws

def serve():
	from aiohttp import web

	app = web.Application()
	app.add_routes(
		[web.get("/ws", websocket_handler), web.static("/", "./dist", show_index=True)]
	)
	print("starting server on port 3000")
	web.run_app(app, port=3000)

And on the frontend I'll just add a snippet of JS for opening the websocket and refreshing when it disconnects. I could add some exponential backoff for if I can't get it to rebuild quick but who really cares. This is JUST for development after all.

const socketUrl = "ws://localhost:3000/ws";
let socket = new WebSocket(socketUrl);
let isReconnecting = false;

function connect() {
	socket = new WebSocket(socketUrl);

	socket.onopen = () => {
		console.log("LiveReload: Connected");
		if (isReconnecting) {
			window.location.reload();
		}
	};

	socket.onclose = () => {
		console.log("LiveReload: Disconnected. Attempting to reconnect...");
		isReconnecting = true;
		setTimeout(connect, 100);
	};

	socket.onerror = (_err) => {
		socket.close();
	};
}

connect();

Perfect. We've got a simple and working setup!