On this page
The Architecture of this Website
Warning: I have since rewritten the entire website without Dioxus. The design is ultimately the same, but it uses a custom static-HTML generator with optional post-processing steps such as running prettier, compressing outputs and minifying HTML/CSS/JS.
The end result is smaller output files, a simplified build process and much more control over the whole system.
See the full merge request here: https://git.tobydavis.dev/tobydavis-dev/tobydavis-dev/-/merge_requests/6
Why Rust, Dioxus and Typst?
The typical advice for building a personal or blog site is to use a static-site generator like Hugo or Zensical, which convert Markdown files into HTML which can be statically served through GitHub Pages, for example.
I’m sure using one of these static-site generators would make my life significantly easier, but where’s the fun in that?
So, let’s make things a bit more complicated.
Rust.
You may be asking yourself, “if you’re going to the hassle of doing this without a normal static-site generator, why not use a normal web framework?”
Good question.
Dioxus
Dioxus is a Rust-based framework for full-stack development in Rust. Dioxus allows you to write a single application which can be served on the web, compiled into a native desktop application and even mobile apps.
While I do not need those features for a personal website, Dioxus’ ability to perform server-side rendering, build static pages and run WASM on the web seemed quite cool. I may also use it for future projects that require more involved user interfaces, so this seems like as good a chance as any to learn how it works.
Typst
Markdown is great for translating human-readable formatting tokens into rendered HTML, but can be quite limiting when it comes to more advanced typesetting requirements. That’s where Typst comes in — it’s an incredibly powerful typesetting language, and it supports HTML export1. This allows me to do things like this:
#{
import "@preview/quill:0.7.2": *
import "@preview/physica:0.9.8": *
figure(quantum-circuit(
scale: 150%,
lstick($ket(0)$), $H$, ctrl(1), rstick($(ket(00) + ket(11)) / sqrt(2)$, n: 2), [\ ],
lstick($ket(0)$), 1, targ(), 1,
))
}Integrating Typst into my website was not simple, but it allows me to make aesthetic diagrams much more quickly.
Devenv
While not an architectural decision, I thought it worth noting that I’m using devenv to configure the build environment and install the packages I need. I can pin package versions for this project specifically and don’t have to worry about conflicting versions impacting other projects.
Website Architecture
Static Content
Some files and assets can be served statically from the public/ directory and do not need to change frequently, if ever. For example, /robots.txt and /.well-known/security.txt are served from there as static files.
Dynamic Endpoints
Some “files” make more sense to generate dynamically on request: specifically, /healthz, /sitemap.xml and /.well-known/webfinger.
Router::new()
.route(
"/healthz",
get(|| async { "ok" })
)
.route(
"/sitemap.xml",
get(tobydavis_dev::misc::sitemap::sitemap_handler),
)
.route(
"/.well-known/webfinger",
get(tobydavis_dev::misc::webfinger::handler),
)
// ...The page’s sitemap is generated dynamically by combining the static routes defined in the Routes enum with the dynamic list of blog posts discovered in the content directory.
The webfinger endpoint looks up the ?resource= parameter from the query in a list of accepted values, returning 404 if it is not valid.
Page Rendering
Any requests that fail to match a predefined route falls back to Dioxus’ server-side rendering functionality.
Standard pages like /, /contact and /about are hard-coded with Dioxus’ rsx! macro, which allows you to write HTML-esque layouts with Rust’s struct construction syntax. rsx! {
h1 { "Welcome to Dioxus!" }
h3 { "Brought to you by {author}" }
p { class: "main-content", {article.content} }
}
Rust’s web frameworks are prone to creating enormous WASM bundles due to the poor browser-side support for common functionality. Dioxus can be configured to produce relatively small () bundles, but that is still quite a bit larger than what standard web frameworks produce.
Fortunately, so long as the entire website is free of server-side dynamic content, Dioxus can compile only the server binary and avoid building any WASM at all. HTML is produced server-side and sent to the client to render, with no additional binaries required. The home page is (at the time of writing) — not the smallest website you’ll find, respectable nonetheless.
Blog Post Architecture
Each blog post is given a unique ID and lives in its own directory with at least the following files:
config.tomlmain.typ(or another entry point)
The config file outlines some basic metadata, and the entry point serves as the base file for Typst to compile. I’ve built a custom template file with some handy formatting modifiers and helper methods for writing blog posts which is pulled in at the top of each post.
I want the system to update live if a new blog post is added (see the CI/CD section below) while still maintaining a cached version of the metadata for each blog post to avoid re-parsing everything every time. To do this, a filesystem watcher triggers a cache invalidation on modification so that the next request re-scans the directory and see the latest information. Whatever that scan finds is added back into the cache so that any subsequent requests do not incur a full filesystem scan.
When a blog post is requested, the entry point file is compiled into HTML and cached against the hash of its content folder.
Cloudflare
My website is proxied by Cloudflare, which means I can use their caching abilities to get significantly better response times and loading performance across the planet than if I were serving it from my tiny VPS.
An Axum middleware layer injects Cloudflare Cache-Control headers into any responses to get them to cache things more aggressively.
CI/CD
The whole website is compiled and bundled into a Docker image which gets served to my GitLab registry.
My VPS has a Docker Compose setup configured with Watchtower to automatically pull the latest website image so that my changes are made available as quickly as possible2.
The same Compose file has Git Sync configured to pull changes made to the content repository such that blog post changes update within about 30 seconds of them being pushed.
Final Thoughts
While none of this is strictly necessary, it presents some interesting problems to solve and works quite well for me. The whole process could be simplified with a custom Typst-based static-side builder, and performance would likely be identical, but if we’re going to engineer something, why not over-engineer it?
Seriously, though, does it really matter if my changes appear within 30 seconds of pushing them? It’s cached by Cloudflare anyway, so it’s limited by the TTL on that…codegen-units = 1, lto = "fat" and opt-level = 3, so builds can take a while.