Migrating my blog from Markdown to Typst

I have recently completed a full migration of my personal blog from its old Markdown-based structure to a modern, Typst-powered system. This change was driven by a desire for better semantic control, a more robust build process, and a commitment to high-performance web standards.

Technical Steps Taken

The migration involved several key stages to ensure that my content remained accessible and that the transition was as smooth as possible.

1. Environment Setup with Nix

To ensure a consistent build environment, I updated my Nix flake to include all necessary tools. This included adding image optimization utilities like imagemagick, optipng, and jpegoptim, as well as ffmpeg for video processing and lighthouse for accessibility auditing.

2. Build System Enhancements (OCaml)

I chose OCaml for my build system because of its strong type system and efficient performance. Key features in the OCaml binary include:

3. Automated Content Migration (Python)

With nearly 70 posts to migrate, I developed a Python script to automate the conversion. This script handled frontmatter detection, tracking pixel removal, and basic syntax translation.

# Detect frontmatter type: +++ (TOML) or --- (YAML)
fm_match = re.match(r'\+\+\+\s*(.*?)\s*\+\+\+', content, re.DOTALL)
is_toml = True
if not fm_match:
fm_match = re.match(r'---\s*(.*?)\s*---', content, re.DOTALL)
is_toml = False

if not fm_match: continue

frontmatter = fm_match.group(1)
body = content[fm_match.end():].strip()

# Strip VG WORT tracking pixels
body = re.sub(r'<img src="https?://[^"]*vgwort\.de/[^"]*"[^>]*>', '', body)

Challenges and What Did Not Work at First

The migration was not without its hurdles. Several issues required creative solutions:

Experimental HTML Export

Typst’s HTML export is still experimental and proved to be quite sensitive. Large code blocks, especially those containing machine-generated JavaScript or complex regex-like characters, would occasionally cause the compiler to hang or crash with exit code 255. I had to simplify some of the larger snippets and use aggressive raw-block wrapping to protect the compiler.

Parallelism Stalls

Initially, the OCaml build tool processed assets sequentially. With hundreds of images, this made the serve command incredibly slow. I had to refactor the OCaml code to background the image optimization process, allowing the developer to see the site immediately while images were optimized in the background.

Typst Syntax Conflicts

Many posts contained characters like @, _, or # which Typst interprets as labels, italics markers, or function calls. I had to implement a batch-processing step to escape these characters or wrap them in raw blocks to prevent compilation warnings or broken layouts.

Why OCaml over Haskell?

While both OCaml and Haskell are excellent functional languages, I chose OCaml for this project for several pragmatic reasons:

Preserving the Web

One of my top priorities was maintaining my existing URLs. By using the original filenames/slugs as the basis for the root-level .html output and providing a robust Nginx configuration for redirects, I’ve ensured that all existing links to /blog/ remain functional.

Content Standards

As part of this migration, I’ve established a persistent standard for AI-assisted content. Every post now includes a transparent disclaimer and a log of the prompts used to generate it, with rules for placement based on whether the content was generated from scratch or an outline.

This article was auto-generated by AI.

Prompts: