How this blog works
A Markdown file, a push, and about nine seconds of CI. No database, no CMS, no framework.
Every page on this site is a file that existed before you asked for it. There is no server rendering anything, no database being queried, and no JavaScript framework rehydrating a shell. You requested HTML and got HTML.
This post is both an explanation and a test case — it exercises every feature the build supports, so if something renders wrong here, it is broken everywhere.
Writing a post
A post is one Markdown file in posts/, named YYYY-MM-DD-some-slug.md. The
frontmatter is small on purpose:
---
title: How this blog works
description: A Markdown file, a push, and about nine seconds of CI.
date: 2026-08-03
tags: [meta, static-sites]
---title and description are required, and the build fails without them. That
is deliberate. description is doing four jobs at once: the excerpt on the
index, the og:description in link previews, the summary in the RSS feed, and
the <meta name="description"> that search engines quote. A post without one
is a post that looks broken everywhere except the page itself.
Everything else is optional:
| Field | Default | Notes |
|---|---|---|
date |
Parsed from the filename | YYYY-MM-DD, treated as UTC |
tags |
none | Free-form, become filter pills on the index |
slug |
Filename minus the date prefix | Set it to change a URL without renaming |
draft |
false |
true builds locally, never publishes |
canonical |
This page | For a post cross-published somewhere else |
Then git push. A workflow builds the site and deploys it to GitHub Pages, and
about nine seconds later the post is live. There is no publish button because
there is nothing to press it on.
What the build does
build.js is roughly three hundred lines and owns the whole pipeline. It reads
the Markdown, renders it, and writes a directory:
$ npm run build
Built 3 posts (1 draft), skipped 1 draft in 812ms → dist/Along the way it produces the things a blog is expected to have but that are tedious to maintain by hand — an RSS feed, a sitemap, per-post Open Graph tags, and JSON-LD so search results show a real title and date instead of a guess.
The build is strict where being permissive would hurt later. Two posts that
would publish to the same URL is an error, not a coin flip about which one
wins. A date that does not parse is an error rather than an Invalid Date in
the feed. The failure happens in CI, before anything is deployed.
Drafts
A post with draft: true is skipped entirely by a normal build. To see one:
npm run serve # builds with --drafts, serves dist/ on :8000Drafts built this way are visibly marked, because the interesting failure mode is not “I forgot to publish it” — it is “I published it and did not notice.”
Code, and why it is not highlighted in your browser
Syntax highlighting runs at build time through Shiki, which uses the same TextMate grammars as VS Code. The browser downloads no highlighting library and runs no highlighting code.
The awkward part of build-time highlighting is usually dark mode: you have picked your colors before you know which theme the reader wants. Shiki can emit both at once, as a pair of custom properties on every token:
<span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span>The stylesheet then picks a side, keyed off the same class the tools family already toggles:
.shiki, .shiki span { color: var(--shiki-light); }
html.dark .shiki span { color: var(--shiki-dark); }So switching theme recolors every code block instantly — no second stylesheet, no flash, no reflow, and nothing fetched.1
Sharing the tools’ chrome
The nav at the top of this page is not this repository’s. It is the same
tools.js that every tool on calvinbrown.dev loads, pulled cross-origin from
tools.calvinbrown.dev. Three tags in <head> and the blog is part of the
family:
<link rel="stylesheet" href="https://tools.calvinbrown.dev/shared/tools.css">
<script src="https://tools.calvinbrown.dev/shared/registry.js"></script>
<script src="https://tools.calvinbrown.dev/shared/tools.js"></script>That single include brings the nav, the dark-mode toggle, the clipboard helper behind every Copy button above, and analytics — all of it configured once, in a repository that is not this one.
The rule the tools follow is that measurement records structure, never content: which post, which tag, which button. The filter box on the index is a good illustration. It reports whether a search matched anything, because that is worth fixing. It does not report what was typed.
The blog adds a few events of its own in the same spirit — scroll depth in quarters, which tag was filtered, which code block was copied. Enough to know whether a post gets read past the first screen, which is the only question I actually have about any of this.
Footnotes
-
The one cost is a slightly larger HTML file, since each token carries two hex values instead of one. On a post this size it is a few kilobytes before compression, which is a good trade for not shipping a highlighter. ↩