Building a Static Blog in Rust
When I decided to start a blog, the obvious choice would have been to reach for Hugo, Zola, or one of the many mature static site generators out there. Instead, I wrote my own in Rust. Here's why, and what I learned.
Why build your own?
I had three reasons:
-
Learning full-stack Rust — I wanted to use axum, Dioxus, and the broader Rust ecosystem end to end. A blog felt like the right scope: real enough to teach me something, small enough not to sink me.
-
Shared components between SSG and SPA — The goal was to render post pages server-side at build time using the same Dioxus components that a WASM SPA would later use for the search UI and draft previews. One component, two rendering paths.
-
Control — My own SSG means I understand every byte it produces.
The architecture
The project is a Cargo workspace with three main crates so far:
crates/markdown— pure markdown-to-HTML library usingpulldown-cmarkandsyntectfor syntax highlightingcrates/components— Dioxus components (no I/O, no platform-specific code)crates/ssg— the build tool that wires everything together
// The markdown crate's public API is intentionally minimal pub fn render(md: &str, opts: RenderOpts) -> String { // pulldown-cmark event stream → heading anchors → syntax highlighting }
The SSG walks content/posts/*.md, parses frontmatter with gray_matter,
renders markdown, calls dioxus_ssr::render on the component tree, and writes
HTML to dist/.
Syntax highlighting at build time
Using syntect driven by two-face's curated themes gives you
Beautiful Code™ with zero client-side JavaScript. The output is inline-styled
HTML — no class-based theming, no extra CSS round-trip.
# The build pipeline in one line cargo run -p ssg -- build --release
What's next
- Phase 2: a Dioxus SPA for client-side search
- Phase 3: a standalone axum API for draft previews and analytics
- Phase 4: deploying to Hetzner with litestream backups to Cloudflare R2
The static-first design means every phase builds on the last without breaking it. If I ever shut down the API, the blog keeps working.
Lessons learned
dioxus_ssris usable but quirky — rendering to HTML strings from a component tree requires aVirtualDomrebuild cycle, and attribute ordering can surprise you.- The
pulldown-cmarkevent stream API is powerful once you understand the Start/End pairing model. - Keeping
crates/markdownpure (no I/O, no global state) made it trivial to test with snapshot tests.