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.
| Flavour | You drop in | You get |
|---|---|---|
| static | index.html | files served at /name/ |
| api | server.py with handle() | logic on every request |
| hybrid | both | a static front end calling its own API |
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.
Servo is a single-file server on my laptop at localhost:8000. It watches ~/servo/ and turns each subdirectory into a URL path. No remote hosting, no production, no framework. The tool and the name are Tavi's (TypeScript on Bun); mine is a Python re-implementation from his design notes, and both run the same apps.
An app is a directory. An index.html makes it static; a server.py exporting handle() makes it an API; both together make a hybrid. No build step, no bundler, no required config.
Every response gets a small CSS palette injected before </head>, so every app follows the active theme without knowing about it, plus a shadow-DOM toolbar with the theme toggle. Why the model works:
- one process, and editing a file is the deploy (API modules reload in place);
- every app reads the same theme variables, so they feel like one environment;
- an experiment is a directory, and deleting it is the teardown.
A half-finished experiment is just a directory I haven't thrown away yet.
Why re-implement instead of running Tavi's? A shared spec beats a shared codebase: each of us bends our own version toward how we work, and where we independently converge, that is usually the right call. With AI in the loop, the cost of re-implementing from a spec has collapsed.
What servo isn't: a framework, a CMS, or a deployment target. No TLS, no auth, one machine by design.