datzero9

Building a Static Blog in Rust

#rust#web#ssg

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:

  1. 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.

  2. 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.

  3. 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 using pulldown-cmark and syntect for syntax highlighting
  • crates/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_ssr is usable but quirky — rendering to HTML strings from a component tree requires a VirtualDom rebuild cycle, and attribute ordering can surprise you.
  • The pulldown-cmark event stream API is powerful once you understand the Start/End pairing model.
  • Keeping crates/markdown pure (no I/O, no global state) made it trivial to test with snapshot tests.