Bureaucracy Core
This article will go into the hows and whys of my new website. Both the general design choice I made by using the GOV.UK Design System and how I ended up making my back-end with Typst and Rust.
The Design
For some time I had been pretty tired of my old website, for one I wanted to not have a split between a blog site with posts and a personal site with a completely different design.
At $WORK
I was tasked with building a internal dashboard which is where I tried to use the GOV.UK design
system (GDS) which is what I started with. I still think it is a great system and generally gives some very nice looking websites though it does reek a bit of bureaucracy.
One of the things that personally got me very interested in the design system is that they have put a lot of work into making the system accessible. And have various notes on how to use each component in a accessible way. This is also the reason that I have chosen to use the Atkinson Hyperlegible for both variable and mono width fonts.
And as a bonus while it does have some elements that are enhanced by having JavaScript enabled it does not really damage the experience to not have it. Which I personally see as a pretty important property.
There was not really any pre-existing component for code so I had to make my own a bit there which was the only thing really missing in my opinion.
I chose a approximation of RAL 2000 for the top bar, mostly just because I like it.
A Semi-Static Site
I have structured the site in a way I would call semi-static the posts such as this one are compiled into a HTML blob which I then insert and send at runtime. This is maybe not the most efficient way to structure a site, but at least for now it works for me.
This means that I use Axum for writing the back-end and serving files. My idea was to serve everything directly from memory so all files and posts are embedded into the binary to be served. In a early design I had the posts generated once at runtime but I went away from that for now because of the above. Though if I want to change back to it there should not be a issue since I have moved the post generation into a separate library.
Typst
I did not really want to use Markdown for my posts, mostly because I wanted to try something different. So I shopped a bit around to see if I could find a alternative that I liked which also had a Rust implementation so I did not have to make one from the bottom up.
After som various other alternatives I ended up with Typst which had also just recently added experimental HTML export.
This ended up being a rather deep rabbit hole, but I think I ended up with something that works pretty well.
Setting up Typst
Using Typst as a library is not documented as much as using it as a binary which is how most will end up using it. So I ended up looking mostly at how Typst themselves used it in various places and ended up making a small implementation of the World
trait that shows Typst how to load files and such.
I also needed a way to embed various metadata into the file so I ended up adding a toml based table commented out in the top of the file:
// title = "Bureaucracy Core"
// date = "2025-04-17"
// filename = "bureaucracy"
// description = "The style and some background to my new website"
The beginning
Typst does the at present time support HTML fragments, so step one was to extract the body HTML element which I then can use. Then I started to do the changes that are possible from inside of Typst.
I set up the heading levels:
show heading.where(
level: 1
): it => html.elem("h1", attrs: (class: "govuk-heading-xl"), it.body)
show heading.where(
level: 2
): it => html.elem("h2", attrs: (class: "govuk-heading-l"), it.body)
show heading.where(
level: 3
): it => html.elem("h3", attrs: (class: "govuk-heading-m"), it.body)
show heading.where(
level: 4
): it => html.elem("h4", attrs: (class: "govuk-heading-s"), it.body)
show heading.where(
level: 5
): it => html.elem(
"h4",
attrs: (class: "govuk-body", style: "font-weight: 500;"),
it.body)
With this code I set the style of the headings according the the GDS, though I also added a extra level that is just bold body text because otherwise one of my posts looked pretty strange since <h5>
was smaller than body text.
I also set up various other simple tags that I use:
show par: it => html.elem("p", attrs: (class: "govuk-body"), it.body)
show link: it => html.elem("a", attrs: (href: it.dest, class: "govuk-link"), it.body)
This will make sure the paragraphs and links have the correct classes and destinations.
The last one I will cover here is lists, both with and without numbers. in Typst unnumbered lists are called enum
and un-numbered are called list
:
show list: li => html.elem(
"ul",
attrs: (class: "govuk-list govuk-list--bullet"),
{ for item in li.children [ #html.elem("li", item.body) ] })
show enum: ol => html.elem(
"ol",
attrs: (class: "govuk-list govuk-list--number"),
{ for item in ol.children [ #html.elem("li", item.body) ] })
These show up quite nice and fully working with this small bit of added template code.
- A
- Numbered
- Enum
- A
- Bulleted
- List
Footnotes
I have a habit of using footnotes[^1] and one of the posts I wanted to move over to the new site made use of them. Typst itself has support for footnotes but it is not yet ported to the HTML back-end. So I had to figure it out myself.
Typst has a construct called state
which allows you to save state during the execution of it. It is keyed by a string so I made a small footnote list that contained a map
[^2]:
#let fnl = state("_govuk_footnotes", (:));
This state I then update every time I add a new footnote:
#let footnote(content) = {
fnl.update(d => {
let id = d.len() + 1;
d.insert(str(id), content);
d
});
context {
let id = fnl.get().len();
html.elem(
"a",
attrs: (
class: "govuk-link govuk-link--no-visited-state",
href: "#footnote_" + str(id),
id: "fn_ref_" + str(id)), "[^" + str(id) + "]")
}
}
This code first updates the fnl
state and adds a new entry keyed as a stringified number that starts at 1. It then uses context
to be able to access the state and here it reads the size and writes out a formatted reference to it.
I then format the footnotes in typst as well:
#let format-footnotes() = {
let fnlist = context {
let f = fnl.get()
let content = []
for (idx, footnote) in f {
content += html.elem(
"li",
attrs: (id: "footnote_" + str(idx)),
footnote +
" " +
html.elem("a",
attrs: (
aria-label: "backlink",
class: "govuk-link govuk-link--no-visited-state",
href: "#fn_ref_" + str(idx)),
"↩"))
}
content
};
html.elem(
"div",
attrs: (id: "footnotes"),
html.elem("ol", attrs: (class: "govuk-list govuk-list--number"), fnlist))
}
The most interesting part of that is that I add a backlink back to the place where the footnote is, otherwise it is a simple numbered list.
For now I decided not to have visit state on those links, though maybe I change my mind on that at a later stage.
Syntax highlighting
Syntax highlighting was likely the single feature I spent the most time on since it was a bit of a pain to get working.
Typst does not have support for syntax highlighting in the HTML export as of 0.13.1. This meant that I had to implement it on my own. First part of this was getting Typst to export some HTML that I could work with:
show raw: it => if it.block {
html.elem("pre", html.elem("code", attrs: (class: "lang-" + it.lang), it.text))
} else {
html.elem("code", it.text)
}
So if it is a code block I make pre element with a code element inside of it, the code element also has a class specifying the language I use, this will be used in the next step. If it is not a block we just use a simple code block which should be used for inline code.
For adding the syntax highlights I use the crate syntect
, this is also the crate that Typst uses. But first I need to extract the <pre><code>
blocks that I made above. For this I used the crate lol_html
which is exactly for rewriting HTML.
The input to this function is everything inside of the <body>
tag which I have already extracted with a regex. I have omitted other languages than rust for ease of reading, the other languages are the exact same.
fn rewrite_html(input: String) -> String {
let mut rs_buffer = String::new();
let settings = lol_html::Settings {
element_content_handlers: vec![
lol_html::text!("code[class~=lang-rust]", |t| {
rs_buffer.push_str(t.as_str());
if !t.last_in_text_node() {
t.remove();
return Ok(());
}
let b = std::mem::take(&mut rs_buffer);
let s = htmlize::unescape(b);
let html = syntax_highlight_ext(&s, "rs");
t.set_str(html);
Ok(())
}),
],
..lol_html::Settings::new()
};
lol_html::rewrite_str(&input, settings).unwrap()
}
With this code we pull out everything that is inside of the <code
class="lang-rust">
tag, you have to check if it is the last in text node otherwise you will not get all in the same buffer.
We then have to actually apply the theme:
fn syntax_highlight_ext(content: &str, ext: &str) -> String {
let content = content.trim();
let ss = SyntaxSet::load_defaults_newlines();
let sr = ss.find_syntax_by_extension(ext).unwrap();
let mut ts = ThemeSet::load_defaults();
let theme = ts.themes.get_mut("Solarized (light)").unwrap();
theme.settings.background = None;
let html = highlighted_html_for_string(content, &ss, sr, theme).unwrap();
// strip sorrounding <pre> tag.
let (_, html) = html.split_at(39);
let html = html.to_owned();
let (html, _) = html.split_at(html.len() - 7);
// Trim newline at start.
let html = html.trim().to_owned();
html
}
This is mostly boilerplate for loading up a theme and applying a highlight with Syntect. We also have to trim a <pre>
tag that have been added otherwise it ends up looking rather since it adds its own background. I was unable to figure out how to stop that in other ways.
All in all this then makes the HTML you see in the Rust code blocks here. Syntect does not have a highlighter for Typst by default as far as I can tell so those blocks are not highlighted.
Rust
The rust side is built up using axum for the routing and serving of the page. And then maud is used for templating all the HTML. Esspecially Maud I ended up liking a lot while I used it, I really like how it stays in the Rust code, which is nice as long as I am the only writing it.
Other than that the Rust code for the runtime of the page does not do a lot of interesting things compared to getting Typst into a box that fits my page.
Conclusion
All in all I am quite happy with how the site turned out, its my first time setting up a proper side that is not just some handwritten HTML grids so there are probably oddities in how I have done it. As always if you have any comments you are welcome to send them to the mail listed in about.
The source code for the site can be found on SourceHut here: https://git.sr.ht/~erk/hs.