home/about

About this site

This site was built by hand with Rust, Maud, and Axum.

That is, this site is a simple statically-linked Rust app.
No React, no Node, no bundler. Just cargo build.

Here's a quick summary of this site's stack:




HTMX + Maud

HTMX allows you to build interactive webpages with exceedingly simple code. Instead of transforming JSON from api endpoints into HTML using Javascript, HTMX has your backend return html that is directly shown to the user.

This is great for simple sites. You don't need to write code in both the backend endpoint and frontend json. It plays especially well with Rust, since you are not forced to give up compile-time type validation at the client-server boundary.

This site is too simple for React. Most sites are too simple for React. I hate the Javascript build system, and want a simple, self-contained website. HTMX is the answer.

Check out the HTMX essays if you're interested.
They make good memes, too.

Maud is an HTML templating language implemented as a Rust macro. It doesn't require external template files like Zola, jinja or handlebars, which necessarily force you to leave Rust's type system.

Maud fits into Rust just as naturally as JSX fits into Javascript. Here are a few examples from the docs:

fn maud_demo() -> Markup {

	// Splicing Rust values into HTML is very easy:
	let best_pony = "Pinkie Pie";
	let numbers = [1, 2, 3, 4];
	html! {
		p { "Hi, " (best_pony) "!" }
		p {
			"I have " (numbers.len()) " numbers, "
			"and the first one is " (numbers[0])
		}
	}

	// We can even use control structures
	// like @if and @for inside of our html!
	let user = Some("Pinkie Pie");
	let names = ["Applejack", "Rarity", "Fluttershy"];
	html! {
		p {
			"Hello, "
			@if let Some(name) = user {
				(name)
			} @else {
				"stranger"
			}
			"!"
		}
		p { "My favorite ponies are:" }
			ol {
				@for name in &names {
					li { (name) }
				}
			}
	}

}

It also plays nicely with HTMX. All the logic in the page below is directly visible in the HTML, there is no Javascript file we need to synchronize with our markup.

html! {
	div class="wrapper" {
		h1 { "Search" }

		section {
			form
				hx-trigger="submit"  // When we submit this form...
				hx-get="/search"     // ...call this endpoint...
				hx-target="#results" // ...then find this element...
				hx-swap="innerHTML"  // ...and replace its contents.
			{
				div class="search-group" {
					label for="user-id-input" { "Search query" }
					div class="input-with-button" {
						input
							type="text" name="query"
							placeholder="Enter search query..."
							{}
						button type="submit" { "Search" }
					}
				}
			}
		}

		section {
			h2 { "Results" }
			div id="results" {
				// The contents of this div are replaced
				// with the response we get from `/search`
				div class="placeholder" { "Enter a search query above." }
			}
		}
	}
}

Of course, we can add extra Javascript if we need it.
But for the majority of cases, HTMX is more than enough.

Also see:




Assets and content

Most images, pdfs, etc on this site are checked into git and compiled into the server binary. Why do anything else?

Simple static pages of text (like this one!) are written in markdown and converted to HTML at runtime using markdown-it and a few custom extensions.

// LazyLock makes sure this page is only initialized once.
// We don't want to parse markdown on every request!
pub static ABOUT: LazyLock<HtmlPage> =
	LazyLock::new(|| {
		page_from_markdown(
			include_str!("about.md")
		)
	});

Compile-time markdown parsing would be nice, but I want to avoid build.rs. This will have to do for now.




Dynamic pages like handouts are generated dynamically. Caching is handled by my servable library.
The calling code is fairly simple:

pub static HANDOUTS: LazyLock<HtmlPage> = LazyLock::new(|| {

	// Fetch the handout index every 20 minutes,
	// cache it in memory.
	let index = CachedRequest::new(
		TimeDelta::minutes(30),
		Box::new(|| Box::pin(async move { get_index().await })),
	);
	tokio::spawn(index.clone().autoget(Duration::from_secs(60 * 20)));

	// The top of the handout page is written as markdown,
	// parse it into HTML. This is only done once, since
	// we're in a LazyLock!
	let md = Markdown::parse(include_str!("handouts.md"));
	let mut meta = meta_from_markdown(&md).unwrap().unwrap();
	let html = PreEscaped(md.render());

	// Return a `servable` object that re-builds this html page
	// every 5 minutes.
	HtmlPage::default()
		.with_style_linked(MAIN_CSS.route())
		.with_script_inline(LAZY_IMAGE_JS)
		.with_meta(meta)
		.with_render(move |page, ctx| {
			let html = html.clone();
			let index = index.clone();

			/// async fn that generates the handout list from
			/// the files published by handout CI.
			render(html, index, page, ctx)
		})
		.with_ttl(Some(TimeDelta::seconds(300)))
});



Images are special: every image on this site supports a magic t= url parameter that transforms the image on the server. This is also provided by the servable lib, and requires no extra code:

pub static BELLCURVE2: StaticAsset = StaticAsset {
	bytes: include_bytes!("bellcurve2.png"),
	mime: MimeType::Png,
	ttl: StaticAsset::DEFAULT_TTL,
};

The transformation parameter enables tricks like this:

<img
	class="img-placeholder"
	src="/about/bellcurve2.png?t=maxdim(50,50)"
	data-large="/about/bellcurve2.png"
	style="width:100%;height=10rem"
>

If we include the below JS, img will show a small blurred placeholder while the full image loads.

window.onload = function() {
	var imgs = document.querySelectorAll('.img-placeholder');

	imgs.forEach(img => {
		img.style.border = 'none';
		img.style.filter = 'blur(10px)';
		img.style.transition = 'filter 0.3s';

		var lg = new Image();
		lg.src = img.dataset.large;
		lg.onload = function () {
			img.src = img.dataset.large;
			img.style.filter = 'blur(0px)';
		};
	})
}

We could do something similar with HTMX, but that would require extra CSS and an endpoint that returns <img> markup. I prefer the JS snippet, it's self-contained and embarrassingly simple.




Rough bits

The servable stack still has some rough edges. Here's a quick list of things I'd like to improve: