How this blog is built
A Markdown folder, one Node script, and no framework. The build log for the publishing system you are currently reading.
Contents
The rest of this site is three files: index.html, styles.css, script.js. No build
step, no framework, no node_modules in sight. I liked it that way, and I did not want
a blog to be the thing that dragged in a bundler.
So the constraint I set myself was: adding a post must not make the site heavier for the reader, and must not make the repository harder to understand.
What it does
Every post is a Markdown file in content/blog/. A Node script reads them at build
time and writes plain HTML into blog/. The browser receives no Markdown, no parser,
and no JavaScript that is not already on the homepage.
content/blog/my-post.md → blog/my-post/index.html
→ blog/index.html
→ blog/tags/<tag>/index.html
→ blog/rss.xml, blog/feed.json
→ sitemap.xml, robots.txtThe generated directory is gitignored. It is rebuilt in CI on every push, immediately before the GitHub Pages artifact is uploaded — which is exactly how the résumé PDF on the homepage already worked. I did not invent a pipeline; I added a step to one that existed.
Frontmatter
Each file opens with a small metadata block:
---
title: Ordering is a local property
description: Message ordering is not something a broker hands you globally.
date: 2026-08-05
updated: 2026-08-09
type: article
tags: [Kafka, distributed-systems]
featured: true
draft: false
---type is the interesting field. I write four different kinds of thing — long articles,
short notes, reactions to something I read, and build logs like this one — and my first
instinct was four separate systems. That was wrong. They differ in presentation, not
in structure: same title, same date, same body. One content model with a type field
does the whole job, and the templates decide that a note does not need a reading-time
badge.
The parts worth mentioning
One dependency. marked, for Markdown. It has no transitive dependencies and it
runs only at build time. I parse the frontmatter myself — it is eight known keys, and
pulling a full YAML engine in to read eight known keys is not a trade I wanted.
Headings become anchors. Every ## gets a stable, Unicode-safe id and a permalink,
so you can link someone to the paragraph that matters instead of to the top of a
two-thousand-word page.
Direction is per block, not per page. I write in English and in Persian, sometimes
in the same paragraph. The build tags each block with the direction that most of its
characters actually want, rather than trusting the first-strong-character rule that
dir="auto" uses — which gets a Persian sentence wrong the moment it opens with an
English technical term.
Drafts are not built. A post with draft: true produces no file at all. There is no
page to leak, no URL to guess, and nothing in the sitemap or feeds. Locally I run:
BLOG_DRAFTS=1 node tools/blog/build.mjswhich is the only way a draft ever becomes HTML.
What I deliberately left out
Syntax highlighting, for now. Every option costs either a large build-time grammar
bundle or client-side JavaScript, and monospaced code with real contrast and generous
line height reads fine. If I start posting more code than prose I will revisit it — the
hook is a single function in tools/blog/markdown.mjs.
Comments, analytics, a newsletter, and search. There are a handful of posts here. A search box would be decoration.
Pagination. At some point a chronological index gets silly, but "some point" is a long way from here, and building for it now would mean guessing at a problem I do not have.
The whole publishing workflow
# 1. write
$EDITOR content/blog/something-i-learned.md
# 2. preview
node tools/blog/build.mjs && python -m http.server 8123
# 3. publish
git add content/blog/something-i-learned.md && git commit && git pushStep three triggers CI, which rebuilds the site and the PDF and deploys. There is no step four, and there is no admin panel to log into — which was the entire objective.