liamsnow.com

Fast personal website made with Rust & Typst, hosted on Helios illumos.

  • Started Icon

    Started:

    2026-01-28

  • Project End Date Icon

    Ended:

    2026-02-22

  • Rust Icon

    Language:

    Rust

Generic Link Icon

Homepage

GitHub Icon

GitHub

I’ve been reading other programmers’ blogs for a long time and have made many attempts at my own. Now that I’m a lot better at programming and technical writing, I have a new version of the website which looks good, is fast, accessible, and a joy to write content for.

Features

  • Custom Typst integration with a custom world for blazingly fast build times (~130ms)
  • Hand rolled HTTP/1.1 server + (near) Zero-copy response dispatching via pre-compressed & pre-encoded responses
  • Hot reloading / watcher mode for development
  • SCSS support
  • Continuous deployment (GitHub webhooks trigger self-update)
  • Rayon parallel compilation

Why Build This?

While I could’ve used an existing SSGs (server-side generators), none support Typst, most would require tweaking to run on illumos, and none were exactly what I wanted.

Why Typst?

For about a year in college I wrote all my notes in Obsidian. After so much frustration with Obsidian being slow, having to pay for syncing to mobile, and it being exceptionally hard to write extensions (at the time), I decided I needed something else.

I switched to NeoVim, using Markdown, pandoc, and my NeoVim live-preview extension. I experimented with writing all my notes in LaTeX, but it was just too tedious and time-consuming.

Eventually I discovered Typst. It was the best of both worlds: the power of LaTeX with the ease of Markdown (and scripting!). It completely transformed my note-taking experience.

I waited a long time for Typst HTML output to stabilize, but eventually got impatient, and decided to use it even while experimental. Sometimes it breaks, and it’s not perfect, but its good enough.

URLs & Queries

I saw two ways to handle routing: explicitly defined URLs in files or implicitly defined URLs based of file path. Explicit is simple to implement and makes indexing easy. For example, since /blog needs to know about all the blog posts, it can simply read the file and build links to each post.

I really wanted to do implicit because it was nice to work with, but to do this, I needed a solution for indexes. The cleanest solution I came up with was to require each page to:

  1. Require each page to specify metadata (like Markdown front matter)
  2. Allow pages to query for the metadata of other pages, passing the result when compiling that file as a system input
// metadata for this page
#metadata((title: "..", ..)) <page>

// request metadata for all pages under `/blog/*`
// then, pass the result back as the system input `blogs`
#metadata((blogs: "/blog/")) <query>

#let posts = {
// grab result of query, sort it by update date
sys.inputs.at("blogs", default: ())
.sorted(key: p => p.at("updated", default: "")).rev()
}

This system is both elegant and powerful. It supports blog.typ (needs blog posts), index.typ (blog posts and projects), and even igloo’s project page (blog posts on only igloo, split by version).

The Code

The code is divided up into 3 key modules:

  1. Indexer (could use a better name):

    • Walks the root directory, discovering all files (Typst, SCSS, and other)
    • Reads all files into memory
    • Retrieves the metadata of all Typst pages
    • Guesses MIME types
    • Computes URLs from file paths (as shown above)
  2. Compiler: uses the index (indexer output)

    • Compiles Typst → HTML, SCSS → CSS, and keeps other file as-are
    • Creates routing table (URL path → HTTP response)

      • Requires making responses for each path: uncompressed/identy responses, Brotli compressed responses, 304 not modified, etag
  3. Web: the HTTP/1.1 server

    • Accepts TCP connections
    • Reads headers to find method, path, ETag, and Brotli support
    • Uses routing table to quickly dispatch responses

Note: Both the indexer and compiler leverage rayon to parallelize their steps. This was chosen over Tokio because of its synchronous nature (file reading, Typst compilation, …).

Integrating Typst

My first prototype would simply spawn the Typst CLI as a process. It ran on every file to query the metadata, and then it would be run again to compile the files. I knew that this would always be slow, and getting around that would require me to integrate Typst directly.

Sadly, it was never intended to be used as a library, and it doesn’t make it easy. I found some experiments (1, 2) that succeeded in doing it, but none supported:

  • Introspection: needed to read metadata
  • Good Error Messages: column + line numbers, fancy formatting (what you get in the CLI)
  • Root Directory Handling: content/blog/igloo.typ must read content/_shared/template.typ, which gets access denied (outside root directory), unless a proper root directory is specified

Furthermore, I wasn’t super happy with their implementations. Instead, I opted to reference Typst internals directly. I spent a long time understanding the internals, and was eventually able to build my own Typst World (which supplies everything needed to compile a Typst file, like file IO).

I designed this world with all the features I wanted, and to integrate seamlessly into my codebase. For example, I made it use an in-memory file system, removing all IO and repeated reads. Common dependencies, such as template files, are read once when the program starts (in the indexer), rather than on every compilation.

Retrieving Typst Metadata

As mentioned, I used Typst introspection for retrieving metadata of every page. But it was frustrating:

  1. To introspect a Typst file, it must be compiled
  2. To compile, the world must be set up
  3. The world requires the in-memory file system
  4. The file system requires the indexer to complete

This means that we must have an entire step dedicated to retrieving metadata at the end of the indexer. Not only does that directly slow us down, we just wasted time compiling a file, only to read ~7 constant lines (IE lines with no variables or dependencies).

Can’t we just merge this step with the compiler? No – to be able to compile blog.typ (a page with a query), we must know the metadata of other pages.

Well then, can keep the compilation from the indexer to produce the output HTML? Also no – the compilation is invalid for actual output, because it had no system inputs.

  • we compiled blog.typ without any system inputs in order to retrieve the metadata
  • the actual output must be compiled with the query result in the system inputs

A Custom Parser

My solution is to skip compilation and the Typst world, implementing my own parser that only parses the metadata fields. It took a bit of digging through Typst source code, and mostly fighting their visibility, but was not too hard.

Using this, we can combine the metadata retrieval with the file reading. This reduces complexity and has much better performance.

Other Features

SCSS

I let grass handle most of the work for compiling SCSS, but also expose my in-memory file system to it.

Hot Reloading

Having a hot reload system really helps with writing posts. So I set up a pretty simple system for this:

  1. Inject some code into the website which will connect to a WebSocket. When it receives a message, refresh the page.
  2. Watch for any file modifications in content/ (create, remove, modify)

    1. Upon change, rebuild, then notify WebSockets

I couldn’t use typst watch for a few reasons:

  1. Typst doesn’t know about our dependencies from our custom query system
  2. We need to compile things besides just Typst files

Continuous Deployment

It would be really annoying if I had to SSH into my server, login to the zone, git pull, recompile, and restart the service for every change to my website. So, I created a pretty simple CD system.

I set up a GitHub webhook which POSTs to liamsnow.com/_update. Upon receiving this it will:

  1. Verify the sig/secret
  2. Git pull
  3. Recompile
  4. Stop the service (SMF will restart it)

Prefetch on Hover

I’ve always loved how fast and snappy McMaster-Carr is. So, I decided to bring over one of their best features – prefetching pages on hover. It prefetches pages when you hover over them, so that when you actually do navigate there, its already cached.

Results

I’m super happy with the results. It’s fast, while having a great DX. I get to write posts in Typst, with hot reloading, and continuous deployment.