This website is built on top of my own Rust-based static site generation scripts. I could’ve simply used an off-the-shelf static site generator (think Hugo), but where would the fun in that be? (alskdjfslkdjflskdfj, I need help, I wasted way too much time on this, aaaaaaa). Ok, here we go — this article will go over the different decisions behind this website.
Toggle table of contents
◇ Djot
This content for this website is written in Djot! The main reason behind this choice was a desire for extensibility. While there are a million and one custom templating / component formats built on top of Markdown, I wanted to build the website on top of a format made with extensibility in mind. Enter Djot — a new format cooked up by the creator of Pandoc and Commonmark. The main feature that caught my attention was the ability to attach arbitrary classes/ids to blocks/inline sections (think divs and spans respectively). My code generator is then able to use said classes/ids to guide custom behaviour.
For instance, I can create character asides like this:
{ title="Character aside example" character="lagrange" id="highlighting-test" }
::: char-aside
Meow
:::
◇ Generating HTML
I used the existing html generator from the jotdown crate as a starting point, but made heavy modifications to the code in order to support the features my heart longed for (like the example above!).
Although repeated write!
calls work well enough for generating simple HTML, I felt the need for a more robust templating system when creating more complex elements. The system I ended up with works pretty well, although a bit simplistic feature-wise (which is by design, copium). For instance, the aforementioned character asides are powered by this template:
<aside class="aside" aria-labelledby="{{id}}">
<div class="aside-header">
<img
alt="{{character}}"
src="/assets/icons/characters/{{character}}.webp"
/>
<h3 id="{{id}}">{{title}}</h3>
</div>
{{content}}
</aside>
The template text gets embedded into the final binary (using incude_str!
), then gets parsed at most once (using an OnceCell
), into what essentially boils down to a list of ranges where content should be inserted:
struct Stop<'s> {
label: &'s str,
start: usize,
length: usize,
}
pub struct Template<'s> {
text: &'s str,
stops: Vec<Stop<'s>>,
}
Although there’s a few different ways I can use such a template, the most convenient one looks something like this:
template!("templates/table-of-contents.html", out)?.feed(
out,
|label, out| {
if label == "content" {
write!(...);
Ok(true) // Keep iterating!
} else {
Ok(false) // We're done (for now)
}
},
)?;
Since a TemplateRenderer
is just a struct (that doesn’t allocate at all, mind you), I’m free to keep partially filled templates around until more djot parsing events get processed, without having to keep the partially-filled inputs in memory.
This theme of minimizing allocations when possible is something I’ve been trying to take to heart (even more in the period since). Both djot and LaTeX parsing is done using pulldown parsers (via jotdown and pulldown_latex respectively). That is, my code generator consumes streams of events instead of constructing an in-memory AST.
◇ LaTeX
Most websites I’ve come across seem to use either MathJax or KaTeX for LaTeX rendering. Shipping JavaScript would not be acceptable for this website, therefore MathJax was out of the equation (since as far as I understand, it operates on the client side only). I think KaTeX can be used to pre-render LaTeX blocks on the server (although the sites I’ve seen using it seemed not to do that for some reason). I don’t particularly remember why I didn’t go with it, although I guess the fact it runs on NodeJS would make usage from rust a bit painful. Oh well, I have working math, so that’s what matters!
◇ Gemini
Another cool benefit of using Djot is being able to define links separately from their usage:
Hello there, check out [my link][cool-link]!
[cool-link]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
While cool in and of itself, this means I should (in the future) have an easier time generating gemtext files for the gemini protocol. I won’t go into detail here, as I don’t currently have such a generator working, but gemtext doesn’t support inline links (only on standalone lines), therefore link definition can be reused to have full control over the placement of such links!
◇ Metadata
I don’t think there’s any consensus (nor any built-in way) on how to include document metadata in Djot documents. I do it through a combination of blocks:
The description of each article gets read from
description
blocks. Said description can later be displayed in places like pages listing various articles. Here’s how it looks:{ role=description } ::: According to all known laws of aviation, there is no way a bee should be able to fly. Its wings are too small to get its fat little body off the ground. :::
Other metadata can be included through
config
blocks. There’s no required fields as of now, and some fields are implied by other fields (i.e.hidden
impliessitemap_exclude
). Moreover, not all metadata is stored in the file itself. Properties like thelast_changed
date are taken directly from Git!{ role=config } ``` =toml hidden = ... created_at = ... sitemap_exclude = ... sitemap_priority = ... sitemap_changefreq = ... ```
◇ Tracking modification dates
You might’ve noticed that every article (including this one!) not only shows the creation date of the post at hand, but that of its last edit as well. There are different ways one could approach implementing something like this:
Getting the date directly from the filesystem. While simple in concept, there’s a myriad of issues that come packaged with this idea — the fact it’s not portable over Git is already a deal breaker for me.
Storing the modification date within the metadata of the post itself (i.e. inside the
.dj
file), and updating it manually on each edit. While easy to implement, this solution is too prone to uhhhh, me forgetting to bump up the date on each edit.Automatically getting the last modification date from Git, and storing it inside the metadata. This is the solution I decided to go with, although issues arose along the way.
As there’s no “official” way to store metadata inside
djot
files (and I’m using my own home-brewed format), modifying the metadata programatically would be a bit hacky, and hence something I avoided. I instead store all the last modification dates inside a file at the root of the repo. “But wait, if you’re getting the dates from Git, why store them in the first place?”, I hear you ask. I too, thought I wouldn’t at first. It didn’t take long (that’s a lie, it took me forever) for me to realise I couldn’t access the.git
directory inside Nix derivations, as that would introduce possible impurities (i.e. access to dangling branches and whatnot), and is thus something Nix makes very hard to do in the first place.
◇ Further work
There’s quite a few things this website is missing. Here’s a non-exhausting list:
- rss/atom feeds
- webmention support
- gemini version of the site
- a dark theme