Migration to Typst
After Google Summer of Code 2020, I began working for Nokia. As a full-time software engineer, I lost interest in programming as a hobby. Because I couldn’t really blog about the cool stuff I was doing at work, this site was rotting for almost 6 years.
A DokuWiki update broke my custom theme, and at some point LaTeX stopped working too. I just couldn’t be bothered to fix any of it.
Two things changed my mind: Typst and AI.
1. Typst
For the longest time, I was looking for a good and universal markup language. I tried:
- Markdown, which is too limited.
- reStructuredText, which is slightly less limited Markdown.
- Wikitext, which is much less limited, but only because it counts raw HTML as “markup”.
- HTML, which is way too verbose for writing prose or math.
- BBCode, which makes me feel old just for mentioning it.
- LaTeX, the tenth circle of Hell.
Also, all but LaTeX treat math as a second-class citizen. They require plugins, extra pain, and maintenance, and still use the TeX language anyway.
Typst is so well-designed that every other markup immediately felt obsolete. It’s as simple as Markdown for prose, yet seamlessly integrates advanced typesetting, equations, diagrams, and a proper scripting language. On top of that, compile times are measured in milliseconds.
As of this writing, HTML output is still experimental. Even so, it’s already the most pleasant way to write a personal blog I’ve ever experienced, and by a long shot.
2. AI
Typst alone wouldn’t be enough. I still had to convert tens of articles full of equations from Wikitext to Typst. This was an excellent use case for AI, though.
LLMs are scarily good at coding if they’re tightly constrained, especially when the work is very tedious. I started by writing common Typst code by hand so that every article would only have a minimal preamble that set everything up. Then I wrote a stub article. At that point, GPT 5.5 had enough context to mass-convert all my pages, making only a few minor mistakes in math. The whole process cost me $6 and one afternoon.
One important instruction I had in my AGENTS.md was: “keep text verbatim.” LLMs like to fix typos and rephrase bad English, and I wanted to keep my articles 100% mine.
This is a pattern I’ve been using very successfully for a while now. Write the hardest, least obvious 10% of code yourself and let the LLM do the remaining 90%. It’s not blind vibe-coding if you know exactly where you’re going.
3. The new site
The full source code is available here.
Each article is its own Typst document with a preamble like this:
#import "/common.typ": post, link-self, img, static
#show: post.with(
title: "Migration to Typst",
date: datetime(year: 2026, month: 6, day: 21),
)
post wraps the article in an HTML template and sets some common settings like heading numbering. It also exposes the title and publication date to Python via metadata.
The template is also written in Typst and looks like this:
#html.html({
html.head({
html.meta(charset: "utf-8")
html.meta(name: "viewport", content: "width=device-width, initial-scale=1")
html.title(title)
html.link(rel: "stylesheet", href: static("style.css"))
})
html.elem("body", attrs: body_attrs, {
html.header(...)
html.main({
html.nav(breadcrumbs()),
html.article(...)
}),
html.footer(...)
})
})
It’s a bit unusual compared to PHP, Jinja2, or JSX. Tags are exposed as functions, which you compose using Typst syntax. You don’t write any HTML yourself.
3.1. Build system
The site is built by a simple Python script. The core idea is this:
import json
from typst import Compiler
compiler = Compiler()
for path in content_dir.rglob("*.typ"):
compiler.compile(
...,
sys_inputs={
'path': relative_path, # Example: "blog/typst_migration"
}
)
metadata = json.loads(compiler.query(...))
Important: If you’re compiling many Typst documents, reusing one Compiler instance is monumentally faster.
sys_inputs is a way to send small amounts of data to the compiler. In Typst, the values are accessible through sys.input. Here, I’m passing the document’s path for navigation breadcrumbs.
Querying is asking questions about a compiled document. In my case, I’m extracting what’s in metadata — the document’s title and publication date, which I define in Typst.
3.2. Indexes
Indexes (like the main page) are built in a separate step. Metadata collected from all articles is saved to a JSON file. Its path is passed through sys_inputs and Typst code can load it:
#let pages-data = json(sys.inputs.pages)
3.3. Bundle export
As of this writing, this site is built with Typst 0.14. Just a week ago, 0.15 was released, adding experimental bundle export. A single document can now output multiple files, which sounds very relevant to what I’m doing here. I haven’t tried it yet, though.