185 lines
8.8 KiB
Plaintext
185 lines
8.8 KiB
Plaintext
{ role="config" }
|
|
``` =toml
|
|
created_at = "2025-03-03T18:42:56+01:00"
|
|
```
|
|
|
|
{ role=description }
|
|
:::
|
|
An overview of the inner workings and technical decisions behind this website, including my reasons for choosing [djot](https://djot.net/) over markdown, rendering LaTeX, templating without needless allocations, and more.
|
|
:::
|
|
|
|
# The realm's secrets
|
|
|
|
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.
|
|
|
|
[Hugo]: https://gohugo.io/
|
|
|
|
::: toc
|
|
:::
|
|
|
|
## 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.
|
|
|
|
[Djot]: https://djot.net/
|
|
[Markdown]: https://en.wikipedia.org/wiki/Markdown
|
|
[Pandoc]: https://pandoc.org/
|
|
[Commonmark]: https://commonmark.org/
|
|
|
|
For instance, I can create character asides like this:
|
|
|
|
```djot
|
|
{ title="Character aside example" character="lagrange" id="highlighting-test" }
|
|
::: char-aside
|
|
Meow
|
|
:::
|
|
```
|
|
|
|
{ title="Character aside example" character="lagrange" id="highlighting-test" }
|
|
::: char-aside
|
|
Meow
|
|
:::
|
|
|
|
### Generating HTML
|
|
|
|
I used the existing html generator from the [jotdown][] 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!).
|
|
|
|
[jotdown]: https://docs.rs/jotdown/latest/jotdown/
|
|
|
|
Although repeated [`write!`][write!] calls work well enough for generating simple HTML, I felt the need for a more robust templating system for 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:
|
|
|
|
```html
|
|
<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!`][include\_str!]), then gets parsed at most once (using an [`OnceCell`][OnceCell]), into what essentially boils down into a list of ranges where content should be inserted:
|
|
|
|
```rust
|
|
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:
|
|
|
|
```rust
|
|
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.
|
|
|
|
[write!]: https://doc.rust-lang.org/std/macro.write.html
|
|
[include\_str!]: https://doc.rust-lang.org/std/macro.include_str.html
|
|
[OnceCell]: https://doc.rust-lang.org/std/cell/struct.OnceCell.html
|
|
[pulldown_latex]: https://github.com/carloskiki/pulldown-latex
|
|
|
|
### 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 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!
|
|
|
|
{ title="The ever elusive spider" character="lagrange" id="the-ever-elusive-spider" }
|
|
::: char-aside
|
|
$`\ddot \ddot \ddot \ddot \omega`
|
|
:::
|
|
|
|
[MathJax]: https://www.mathjax.org/
|
|
[KaTeX]: https://katex.org/
|
|
|
|
### Gemini
|
|
|
|
Another cool benefit of using Djot is being able to define links separately from their usage:
|
|
|
|
```djot
|
|
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][gemini]. 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!
|
|
|
|
[gemini]: https://en.wikipedia.org/wiki/Gemini_(protocol)
|
|
|
|
### 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:
|
|
|
|
```djot
|
|
{ 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` implies `sitemap_exclude`). Moreover, not all metadata is stored in the file itself. Properties like the `last_changed` date are taken directly from Git!
|
|
|
|
````djot
|
|
{ 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][moonythm-last-modifications-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]{ role="strike" }) 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.
|
|
|
|
[moonythm-last-modifications-file]: https://git.moonythm.dev/prescientmoon/moonythm/src/branch/main/last_modified.toml
|
|
|
|
## 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
|
|
|
|
[rss]: https://en.wikipedia.org/wiki/RSS
|
|
[atom]: https://en.wikipedia.org/wiki/Atom_(web_standard)
|
|
[webmention]: https://indieweb.org/Webmention
|
|
|