Rebuilding this site in Rust
The previous version of this site was a docker-compose file with four
services: a React client, a Node gateway speaking gRPC-web, a protobuf package,
and an nginx reverse proxy. There was also a Rust crate that compiled to WASM,
and, at one point, a Kafka consumer.
It served five static pages.
I want to be clear that I am not embarrassed by this. It was 2020, I was learning gRPC and WASM and container orchestration, and the site was the excuse. The architecture was the portfolio piece. That's a legitimate reason to build something.
But the bill came due. By the time I opened it again, node-sass wouldn't
compile against any Node version I had installed, Material-UI v4 had no upgrade
path that didn't amount to a rewrite, and the ejected Create React App config
meant I personally owned about ten files of webpack 4 configuration. Dependabot
had opened seventeen pull requests. The site had become unmaintainable in the
specific way that a stack you no longer remember becomes unmaintainable.
What replaced it
One Rust binary. Axum for routing, Maud for HTML.
Router::new()
.route("/", get(pages::home))
.route("/writing", get(pages::writing))
.fallback(pages::not_found)
.layer(CompressionLayer::new())
Maud is the part worth talking about. It's a procedural macro that turns HTML-shaped syntax into Rust:
html! {
section class="hero" {
h1 { "Aneesh Saripalli" }
p class="lede" { (subtitle) }
}
}
This compiles to string-building code with no runtime template parsing, no template files to ship, and no template cache to warm. More importantly it is type-checked: a mistyped variable is a compile error, and there is no stringly-typed layer between me and the output. The entire class of "template renders but the variable was undefined so it silently emitted nothing" is gone.
The tradeoff is real: markup lives inside Rust, so there's no handing a
.html file to a designer, and recompiles are the edit loop. For a site I'm the
only author of, that's a trade I'll take. If I were collaborating on the markup
I'd reach for Askama and accept losing some type safety.
The part I actually care about
The old site rendered nothing on the server. A crawler received an empty <div id="root"> and a JavaScript bundle. Google does execute JavaScript now, but it
does so on a separate, slower, lower-priority pass, and it doesn't always
finish. If your content only exists after hydration, you're gambling.
The new site emits complete HTML. Not "server-rendered then hydrated" —
complete, final, no-JavaScript-required HTML. curl and Googlebot receive
exactly the same bytes a browser does.
That constraint turned out to be clarifying rather than limiting. It's the subject of its own post.
Was it worth it
The honest accounting: I replaced roughly 15,000 lines of configuration and dependency graph with about 500 lines of Rust. The binary starts in milliseconds, holds a few megabytes of RSS, and has a dependency tree I can actually read.
I also gave up the free hosting I'd have gotten from a static export. A live Rust process needs somewhere to run, and that's a few dollars a month plus actual operational surface. I decided the optionality was worth it — being able to add a real endpoint later without restructuring anything.
Ask me again when something pages me at 3am.