The problem with publishing stacks
Most engineering blogs live inside a platform: Medium, Substack, a headless CMS, a static-site generator with a plugin ecosystem. Those tools are fine, but they all make the same implicit promise: you can always add more later. More plugins, more themes, more JavaScript, more configuration. That promise costs something. The more a tool can do, the more you have to understand before you trust what it actually does.
We wanted a blog that we could read end-to-end in an afternoon. Not read the docs — read the entire implementation.
So we wrote 200 lines of Python, called it build_blog.py, and made that the blog.
How the build works, step by step
Here is what happens when you run python3 scripts/build_blog.py from the repo root.
Step 1: Clean the output. The script deletes the blog/index.html, the blog/tag/ directory, and every generated subdirectory inside blog/posts/. The source Markdown files (.md) are left alone because they live in blog/posts/ too. Only the compiled HTML subdirectories are removed.
Why delete before rebuilding? So that removing a post's .md file automatically removes that post's output on the next run. No stale pages, no manual cleanup.
Step 2: Read and validate every Markdown file. For each .md file in blog/posts/, the script reads the YAML front matter — the block of metadata between the first two --- lines. It checks that five fields are present: title, date, author, tags, and excerpt. If anything is missing or the excerpt exceeds 160 characters, the script exits non-zero and names the file and the failing field. The build fails loudly rather than silently producing a broken page.
Step 3: Normalize tags. Tag strings go through four transformations: lowercase, spaces become hyphens, non-alphanumeric characters (except hyphens) are stripped, repeated hyphens collapse into one. This means iOS Apps and ios-apps would map to the same URL — a collision. If any two raw tag strings across all posts normalize to the same slug, the build exits with an error before writing any files. Two identical raw strings across different posts are fine; one canonical form is required.
Step 4: Render Markdown. The script converts Markdown to HTML using a line-by-line state machine. It handles a limited subset: H1–H3 headings, bold, italic, inline code, fenced code blocks, links, unordered lists, and horizontal rules. There is no CommonMark compliance and no general Markdown parser. The constraint is the feature: a smaller parser means fewer surprises.
Step 5: Write HTML files. For each post, the script creates a directory at blog/posts/{slug}/ and writes index.html inside it. For each tag, it creates blog/tag/{slug}/index.html listing all posts with that tag. Finally it writes blog/index.html with all posts sorted newest-first.
Step 6: Update the sitemap. The script reads the existing sitemap.xml, strips any URLs under https://foculoom.com/blog/, and appends fresh entries for the blog index and every published post. Non-blog URLs are preserved unchanged.
Why no dependencies?
The build script imports only Python's standard library. No pip install. No requirements.txt. No virtual environment.
This is a practical constraint, not a philosophical one. The script runs on whatever Python 3 the CI runner has installed, with no setup step. If a dependency breaks or its API changes, nothing here breaks with it. The tradeoff is that we write more code ourselves — but the code we write is directly visible in the repo, easy to read, and small enough to reason about completely.
What this pipeline cannot do
The supported Markdown subset does not include tables, footnotes, task lists, or nested lists. If a future post needs those, we will add support explicitly rather than pulling in a general-purpose parser. Every addition is a deliberate choice, not a side effect of a dependency update.
This is also not a preview server, a watch mode, or a development workflow. It is a build script. Run it, check the output, commit if it looks right.
That is the full pipeline. The rest of the blog will use it. Future posts will cover product decisions, deployment lessons, and the engineering work behind Foculoom's apps. If you want to discuss anything here, reach out.