Most of the posts on this blog say the site is "a servo app." That's probably opaque if you haven't used servo, so here's the short version.
One server, many small sites
Servo is a single-file server that runs on my laptop at localhost:8000. It doesn't host anything remotely, it doesn't ship to production, and it doesn't try to be a framework. It watches one directory, ~/servo/, and turns each subdirectory in there into a URL path.
Credit where it's due: the tool and the name are Tavi's. He wrote the first version in TypeScript on Bun. What I describe below is my Python re-implementation, built from his design notes. Both versions run the same apps, that's the point of the arrangement, and he's written up the rationale better than I could.
~/servo/ is reachable at /<name>/. That's the whole routing system.An app is just a directory
A minimal servo app is one file. Drop an index.html into ~/servo/hello/ and visit localhost:8000/hello/. That's the whole pipeline. There's no build step, no bundler, no config file strictly required.
If you want the app to show up with a specific type in the dashboard, add a servo.json:
{"type": "static"}
That's what most of my apps are, the blog, the photo gallery, a couple of experiment pages. The files get served, the URLs work, and that's it.
When a directory needs logic
The other flavor is API. Drop a server.py into the app directory and export a handle() function:
# ~/servo/echo/server.py
def handle(req):
return {
"status": 200,
"headers": {"content-type": "application/json"},
"body": {"you_said": req.get("query", {})}
}
Then ~/servo/echo/servo.json:
{"type": "api"}
Servo imports the module, calls handle() on every request, and returns whatever you give back. Static files in the same directory still get served normally, so a hybrid app is just a server.py next to an index.html that calls into it.
Theme injection is the best part
Every response from servo gets a tiny chunk of CSS injected before the closing </head>. It defines a fixed palette on :root:
--bg, --surface, --fg, --fg2, --fg3,
--border, --hover, --accent, --accent-light,
--red, --green, --radius, --content-width
Every servo app that uses those variables automatically picks up whatever theme is active. Switching from light to dark changes every app on localhost:8000 at once, without any app needing to know about it. This blog, for example, has zero hex colors, everything resolves through variables.
A small toolbar component (<servo-toolbar>) is also injected, with a theme-toggle button. It uses shadow DOM, so nothing leaks into the app's styles.
Why I like this model
- One server process. I start it once in the morning; everything I'm working on is reachable through it.
- No deployment. Editing a file is the deploy. This is obvious for static apps but equally true for API ones, servo reloads
server.pymodules in place. - Consistent look. Because every app reads the same theme variables, the blog and the note-taking app and the poll widget all feel like part of one environment, even though they share nothing else.
- Disposable. A half-finished experiment is just a directory I haven't thrown away yet. When I do, the URL stops resolving and nothing else cares.
Why I didn't just run Tavi's
The obvious question: if Tavi already had a working servo, why write a second one? The short answer is that sharing a spec instead of sharing a codebase is what gave me room to move at all. Running his version would have meant inheriting every implementation decision, Bun, TypeScript, his choice of file watcher, his module loader, and any experiment I wanted to try would either pass through his review first or live as a fork nobody else could run.
Writing my own from the design notes was fast. With AI in the loop, the gap between "I have a spec" and "I have a working first version" has collapsed, the question isn't really "can I afford to re-implement" anymore, it's "what do I want my re-implementation to look like." And the adaptation comes for free: as it takes shape I'm already bending it toward the way I work, the apps I have in mind, the file layout I prefer. Starting from someone else's finished codebase and re-shaping it to fit would have been harder, not easier.
The bigger win is that I understand the tool in a way I wouldn't have if I'd just run Tavi's. Building it surfaces every decision, what gets injected, when, in what order, how errors propagate, where config lives, and having made those decisions myself I know exactly where servo can be pushed and where it can't. That knowledge is the thing that tells me which side projects are a good fit for it. I'd be guessing without it.
Tavi gets the same thing in the other direction. When we each try a different approach to the same problem, the one that survives on both sides is usually the right answer.
Maybe at some point there'll be a servo to rule them all: a canonical implementation that inherits the best calls from both. I don't rule it out. For now, though, each of us developing our own version is the gift that keeps on giving. Every bit of divergence tells us something we'd never have learned with a single codebase, and none of the convergence has been forced. When we agree, it's because we independently landed in the same place.
What servo isn't
It is not a framework, not a CMS, not a deployment target. It doesn't do TLS, authentication, user accounts, or anything you'd want from a public-facing server. It lives on one machine by design. If I want something remotely hosted, I build it somewhere else and copy the learnings back.
The whole server is a single Python file, small enough that when I want it to do something new, I can usually read the relevant code path, add a few lines, and keep going. That's the part I keep coming back to. It's less a piece of infrastructure and more a place where my side projects live.