—
This brief article goes over my rewrite of this website using Typst and bash.
This is the second iteration of this site. Initially, it was written with zine which, while it wasn’t bad to work with, wasn’t built for my use-case and constrained what I wanted to do too much.
Upon considering a rewrite, I’d been using Typst to typeset my math and physics notes for a couple of years and seeing it had (very) experimental HTML support, I felt it’d be good to try my luck with.
As I had a bit of free-time, I decided to establish some goals/constraints on my rewrite:
As close to “pure” Typst as possible.
Full control over all parts of the output.
<html></html>, we overwrite the default which allows us to write the <head>.html.frame to embed the content rendered as SVG.Capable of rendering mathematics client-side.
Common header/footer, frontmatter-like article abstractions, etc.
importing and modules.Should be rendered consistently across browsers and platforms.
After some brief searching, I found a couple of existing Typst static-site generators (see gh:Glomzzz/typsite, gh:kokic/kodama, gh:kawayww/tola-ssg, gl:pcoves/theta)[1]. But rather than use them, I decided to have more fun and write one myself (the results of which you are looking at).
I accomplished all of the above goals to a reasonable degree within ~three days. Below I detail the basic architecture as well as some points I found interesting.
I dislike over-complexity, as everyone should, though I have my fair share of it. In evaluating the above goals, I wanted to use as minimal of a “stack” as possible: Typst + bash.
I have a fairly concise shared library _lib.typ wherein I define the “templates” for the pages. For the writings, such as this one, the templating function writing-format (which in turn uses html-format) consumes a dictionary for details on the document (which also allows for a mostly automatic writing listing on index).
For instance, for this article the setup is as follows:
#import "../_lib.typ": *
#let info = (
path: "001",
date: datetime(year: 2025, month: 11, day: 6),
title: "Developing This Site In Typst",
tags: ("typst", "site"),
)
#show: writing-format.with(info)
The above code-block is dynamically read from the source files during compilation, which I find quite cool. Of course, that means that as the source changes, de-sync may occur, but in this case, as it is the top of the document, it’s rarely going to change (except when I modify the info format or contents)[2].
For the index.typ, I directly use the parent template html-format which sets up the entire HTML structure.
#let html-format(title, content) = context {
show: html.html.with(lang: "en")
html.head({
html.meta(charset: "utf-8")
html.meta(name: "viewport", content: "width=device-width, initial-scale=1")
html.link(href: "/index.css", rel: "stylesheet")
html.title(title + " - nukkeldev")
})
html.body({
page-header
content
page-footer
})
}
While rewriting, I found (either on my own, from other codebases, or from the forum) a few cool features with the current HTML exporter implementation that I didn’t find officially documented elsewhere.
When a Typst element is labeled and directly referenced (i.e. with a link), the output is id'd to that label.
outline is present, are given ids in the form loc-# if a label isn’t attached (the outline properly links to the custom labels if they are present).<html> structure for the document, we are given a basic one. Unfortunately, when using a custom HTML root, some features (i.e. footnote) are disabled.One thing we can do with show rules (+ regex) though, is replacing the content of links.
When preferable I’d rather not have to type out the full #link("...")[...] for links that are for common things (i.e. github repos, commits, etc.) To do this, I used AI for the one of the few good uses: writing regex.
^https://(github\.com|gitlab\.com|codeberg\.org)/([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)(/(commit|pulls?|issues)/([0-9a-fA-F]+))?/?$
That beauty matches repositories, commits, pull requests, and issues from Github, Gitlab, and Codeberg. I can match it once in the show rule and again in the body to get the capture groups to format the links as you saw above when I first mentioned equation rendering.
Overall, this rewrite has been quite fun so far (considering it’s been three days) and I’ll definitely keep refining and adding more features to this. There is still quite a bit of work to do on the styling; I hate CSS.
—