Colophon

This site is built with a custom static site generator written in Rust. Unlike most static site generators that model content as flat files with metadata, this one uses an RDF graph as its core data model. Every content file becomes a set of triples in an in-memory graph database, and the relationships between entities–posts, topics, people, sources–are first-class graph edges rather than ad hoc metadata.

The graph

The heart of the system is oxigraph, an RDF triple store and SPARQL engine written in Rust. At build time, every content file is loaded into an in-memory oxigraph store. Front matter keys map to RDF predicates via a vocabulary configuration, using standard ontologies like Dublin Core, FOAF, and SKOS. When a post declares subject: ['@topics/ai'], that becomes a dc:subject triple linking the post to the AI topic entity, and an inverse subject_of triple is automatically generated linking back.

Templates query the graph at render time via SPARQL, which means relationships like “all posts about this topic” or “all sources by this person” are derived from the graph rather than hard-coded.

The build also computes a “referenced set” via breadth-first search from root types (posts and pages). Entities not reachable from any published post are automatically hidden from listings, RSS, and the sitemap–their pages still exist, but they’re undiscoverable without a direct URL. This means I never have to manually manage which topics or people appear on the site; publishing a post that references them is sufficient.

Content model

Content files are Markdown with YAML front matter, parsed by pulldown-cmark (a CommonMark parser written in Rust). Every content file has a type, and types form a hierarchy: a post is a kind of prose, which is a kind of entity. Template resolution walks this chain, so specific types can override general templates.

The site supports a custom inline entity reference syntax that creates both a hyperlink and a graph edge from within the prose body. For example, writing @[Dario Amodei]("@people/dario-amodei" rel="subject") renders as a normal link to the person’s page while simultaneously creating a dc:subject edge in the graph. A custom syntax pre-processor handles these (and shortcodes) via a character-by-character scanner with an extensible handler registry, replacing the markdown body before it reaches the CommonMark parser.

Templates and rendering

Templates use MiniJinja, a minimal Jinja2-compatible template engine for Rust by Armin Ronacher (creator of Flask and Jinja2 itself). Custom template functions execute SPARQL queries against the graph, and custom filters handle things like date formatting, language-aware sorting via ICU4X (Unicode’s official Rust internationalization library), and resolving inline entity reference placeholders to their final URLs.

Internationalization

The site is bilingual: English and Chinese. Each content file is a single file containing all languages. Localized front matter fields use .lang suffixes (e.g., title.en, title.zh), and body text uses @lang fences to delimit language-specific sections. All UI strings go through YAML translation files. Only language-neutral fields (references, dates, booleans) create graph edges; localized strings become language-tagged RDF literals. ICU4X handles locale-aware collation for sorting content correctly across both languages.

CSS and assets

A single CSS file provides all styling, processed at build time by Lightning CSS (the Rust CSS parser and minifier built on Mozilla’s cssparser crate, the same parser used by Firefox). The output is minified and fingerprinted with a SHA-256 hash for cache busting. Light and dark themes are handled via CSS custom properties with a [data-theme] attribute toggle.

Infrastructure

The dev server uses axum (the Tokio team’s web framework) with file watching via the notify crate for automatic rebuilds on content changes. The source lives on Codeberg and deploys to Cloudflare Pages via wrangler.

Other dependencies

A few more crates worth mentioning: clap for the CLI, serde for serialization, anyhow for error handling, sha2 for asset fingerprinting, and walkdir for filesystem traversal.

Why build a custom SSG?

Mostly because the content model I wanted–a typed entity graph with first-class relationships, inverse predicates, and automatic discoverability–doesn’t map cleanly onto any existing static site generator. I used Hugo for the previous version of this site and spent more time fighting its taxonomy system than writing. Building a custom tool meant I could model the domain exactly as I wanted, and Rust made it fast enough that the full build (parsing, graph construction, SPARQL queries, template rendering, CSS minification, RSS, sitemap) completes in under a second.