Blog
The knowledge base itself: markdown files, a zero-dependency build
Overview#
Every post here is a markdown file with front matter, nothing else. No database, no framework, no package to install for the build itself to run. build.mjs, about 1,400 lines of plain Node, reads every file, runs its own small markdown parser, and writes out the whole site: every post page, every project page, search, RSS, sitemap, robots.txt. If Node exists, the Blog builds.
Site structure#
Blog/
posts/ one .md file per article, front matter on top
projects/ one .md file per product page (this one included)
data/ now.md (the /now/ page) and timeline.json
assets/ theme.css (shared across the whole domain) + blog.css
admin/ the Decap editor: index.html, config.yml, a vendored CMS bundle
scripts/ sanitize-check.py and the rest of the pre-publish checks
build.mjs ~1,400 lines of Node, zero dependencies
dist/ the generated site - wiped and rebuilt on every run, never edited directly
node build.mjs reads every file in posts/ and projects/, runs each one through a small hand-rolled markdown parser (headings, lists, tables, fenced code, Mermaid diagrams, callouts), and writes the whole site into dist/: every post page, every project page, the view pages (recent, pinned, archive, timeline), search, RSS, sitemap, robots.txt. It fails loudly rather than guessing when something is wrong: a post missing topic:, a project: that doesn't match a real file in projects/, or the retired collection: key all stop the build with a specific error, not a warning buried in the output. A build that silently tolerates bad front matter is a build that lets the architecture rot one post at a time, so this one doesn't.
A project page like this one assembles most of itself from the same source files. The frontmatter above (stack, tags, topics, roadmap, and so on) drives the hero card, the tech list and the roadmap section; the body below only supplies the prose. The development log, the related-articles list and this project's entry on /projects/ are all generated by build.mjs from whatever other files reference project: blog or topics: [...], never hand-maintained here.
The three layers#
Content here is never a flat list. Projects are the products (this one included) and get the largest visual weight. Topics, exactly six and fixed in the build script (infrastructure, cybersecurity, software, learning, endurance, life), are what an article is about. Views are just ways to browse the same content differently: recent, pinned, archive, timeline. The sidebar's own font sizes step down across the three layers on purpose, so the size difference carries the hierarchy without a label explaining it.
An article filed with project: blog appears in this project's own development log automatically. There is never a second list to update by hand; that is the rule this architecture exists to enforce.
Posting through Decap CMS#
For a year, writing a post meant a terminal: hand-written front matter and node build.mjs. In October a graphical editor got added, Decap CMS, reachable at /admin/ behind a second login on top of GitHub's own. A post made through the editor is still the same markdown file with the same front matter, committed to the same repository the build script already reads, so nothing about the underlying model changed to support it. The bundle itself is vendored into admin/decap-cms.js rather than pulled from a CDN at request time, so the rest of the site's Content-Security-Policy stays exactly as strict as it was before the editor existed.
Two collections, mapped straight onto the two folders above. admin/config.yml defines a posts collection (folder: Blog/posts, one form field per frontmatter key: title, date, summary, topic, project, tags, difficulty, pinned, draft, series, part, body) and a projects collection (folder: Blog/projects, the same idea for stack/tags/topics/roadmap/ downloads/github/live/docs). Both run format: frontmatter with identifier_field: title, so Decap writes exactly the markdown-plus-YAML shape build.mjs already expects, with no translation layer in between. publish_mode: simple means every save commits straight to main; there's no draft/review branch workflow, because there's one author and that workflow would be overhead with nothing to review against.
Logging in needs a server, because GitHub's OAuth token exchange has no CORS support. A browser can ask GitHub to authorize, but it can never trade the resulting code for a token on its own. That's the one piece a static site genuinely cannot do by itself, so a small self-hosted service, cms-oauth, does it:
1. The editor opens a popup to cms.fiventhree.com/auth, which redirects to GitHub's own login with a stateless, HMAC-signed state parameter (a timestamp plus a signature over it, verified on the way back, with no session store needed for a single-admin tool, and expiring after ten minutes). 2. GitHub sends the browser back to /callback with a code. The server verifies state, then exchanges the code for an access token directly with GitHub, server-side, so the client secret never reaches the browser, and posts the token back to the popup's opener window via postMessage, in the two-step handshake Decap's GitHub backend expects. 3. From there, every save is an ordinary authenticated commit to main through GitHub's API, made with the logged-in user's own token, not a shared bot account.
Publishing triggers its own deploy, so using the editor really does mean the whole loop happens without a terminal. The same cms-oauth service also carries a /webhook route: GitHub calls it on every push to main, the service verifies X-Hub-Signature-256 against a shared secret with crypto.timingSafeEqual (not a plain ===, which would leak timing information about how much of the signature matched), then runs git pull against an explicit HTTPS URL carrying a fine-grained, pull-only token, never the human's own SSH-based origin, followed by the same deploy.sh static a person would run by hand. No database, no Docker socket; just a bind mount to the same files deploy.sh already touches.
Getting there took four separate, unrelated fixes: a Caddy directive that cannot nest where it was placed, a Content-Security-Policy that blocked the editor's own code (Decap's bundle calls Function() for part of its config machinery, which needs script-src 'unsafe-eval', scoped to just /admin/* so the rest of the site never sees it), a copy-pasted placeholder that was never meant to stay literal, and a container that did not trust the ownership of its own bind-mounted repository. The full account is in Setting up Decap CMS, and the four things that broke.
Keeping the prose honest#
Two scripts run before anything here is trusted to publish. prose-check.py enforces house style across every post: no em dashes, no spaced hyphen standing in for one, and a growing list of stock AI phrasing and intensifiers it flags on sight, checked line by line rather than trusted to memory. sanitize-check.py checks for exactly the kind of detail that should never end up in a public write-up about real infrastructure: IP addresses, VLAN IDs, internal hostnames, an employer's name in a technical context. Both are plain Python, no network access, and both get re-run after anything infrastructure-related changes.
Related reading#
Development log
Every article written against this project, newest first.
Consolidating the project pages, and the YAML bug hiding underneath
Vault and the portfolio were never separate infrastructure, so their project pages became sections of The 5N3 Domain instead. Along the way: a project page with a blank title in the editor, traced to an unquoted colon that a hand-rolled parser never minded and a real YAML parser refused outright.
Setting up Decap CMS, and the four things that broke
Adding a graphical editor to the Blog meant no new database and no format migration, since Decap just commits to the same markdown files. Getting GitHub, Caddy and Docker to actually agree with each other took four separate fixes.
Building this blog
Why this journal is a folder of markdown files and one build script, with no framework, no database and no dependencies.