001 - Developing This Site In Typst

2025-11-06 + tags: typst, site

This brief article goes over my rewrite of this website using Typst and bash.

Goals

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:

  1. As close to “pure” Typst as possible.

    1. Unfortunately, as Typst is only able to produce a single output file, we still need another program to trigger the compilation calls, but a bash script seems sufficient for that.
  2. Full control over all parts of the output.

    1. Typst’s html support allows for both typed and raw access to HTML elements.
    2. If we specify our own <html></html>, we overwrite the default which allows us to write the <head>.
    3. If the HTML exporter does not support a specific Typst feature (i.e. graphics, equations, etc.), we are able to fallback on html.frame to embed the content rendered as SVG.
  3. Capable of rendering mathematics client-side.

    1. This is primarily for performance reasons. Rendering all of the equations to SVGs server-side would be the most ideal scenario as there wouldn’t be any ambiguity in their rendering across platforms. Unfortunately, the 10s of kilobytes per equation is quite discouraging and leaves it only to be used when absolutely necessary.
    2. As of gh:typst/typst [commit: 16de17a1], Typst’s HTML exporter does not support anything related to mathematics as stated above. Luckily, gh:typst/typst [pr: #7206] is working on an initial implementation of getting MathML working in the exporter and it works well.
    3. Alteratively, cb:akida/mathyml is a package to render equations into MathML in pure Typst. Both seem similar feature-wise so I’d prefer to not use another external dependency.
  4. Common header/footer, frontmatter-like article abstractions, etc.

    1. Easy to implement with importing and modules.
  5. Should be rendered consistently across browsers and platforms.

    1. This is out-of-scope for Typst and falls more into CSS styling and fonts, of which Typst allows me to easily work with.

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.

Overview

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.

Templates

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
})
}
Cool Features

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.

Link Replacements

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.

Conclusion

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.