How I Build Things

And why

← Back to index

Knowledge base pattern

Prose content records — principles, decision records — authored in markdown, built to static HTML by a small, exactly-pinned script.

MarkdownPythonJinja2GitHub Pages

Live app →

Overview

Some content isn't an interactive artifact, it's a body of written decisions meant to be read: principles, decision records, this bundle itself. The right authoring format for that is markdown, not hand-written HTML — prose, headers, and cross-references between records are what markdown is for. Markdown to a browser is, unavoidably, a transform step, so this pattern accepts a build rather than pretending one isn't needed.

Use case

A growing set of written records that cross-reference each other (a principle linking to the patterns that follow from it, a pattern linking back to the principles behind it) and share one consistent look across every page. The content is the point; there's no interactivity beyond reading and following links.

When not to use this pattern

Options considered

Hand-authored static HTML, no build step

The same shape as the HTML tool pattern: write each page's HTML directly, share a stylesheet, no generator.

An off-the-shelf static site generator (Jekyll)

GitHub Pages builds Jekyll natively, so markdown-with-frontmatter in gets HTML out with no custom code.

A small, purpose-built script (current)

build.py: markdown + YAML frontmatter in, rendered through Jinja2 templates, out as static HTML — exactly-pinned dependencies (pyproject.toml with ==, a committed uv.lock), matching Minimal dependencies.

Current recommendation

The purpose-built script. It's the only option that gets exact dependency pinning, matches the site's existing look-and-feel without a second templating language, and enforces the one invariant (no dangling cross-references between records) that actually matters at this bundle's scale — without adopting a second language ecosystem to get there.

This pattern's recommendation follows from three of the bundle's principles: Minimal dependencies — exact-pinned Python dependencies and a committed lockfile, not whatever GitHub's hosted Jekyll happens to ship; No egress by default — the build is a pure, offline transformation, bundle and templates in, dist/ out; and Least privilege — the deploy workflow's token is scoped to exactly contents: read, pages: write, id-token: write, nothing broader.

Architecture

flowchart LR
    A[Push to main] --> B[GitHub Actions]
    B --> C["uv run build.py\nmarkdown + Jinja2 -> dist/"]
    C -->|check_links fails the build\non a dangling cross-reference| D[GitHub Pages]
    D --> E[Browser]