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.

← All writing