A minimal static site generator within a few hundred lines
This blog runs on a hand-rolled static generator with no framework. Here is how it works: strict frontmatter validation, a constrained Markdown renderer, and plain HTML templates.
In the previous post we argued that marketing sites should be static. To prove the point, we built this blog that way: no framework, no bundler, no CSS preprocessor. The entire generator is one Node script plus two small library modules — a few hundred lines of logic, on top of the HTML templates themselves.
Here's why, and how it works.
Why not use an existing SSG?
It's a fair question. There are excellent static site generators — and for many sites they're the right tool. But for a small blog that we maintain, the cost of a generator framework is the same as any other framework: version churn, plugin ecosystems, and a build that behaves differently than the code you actually wrote.
A minimal generator has real advantages:
- You can read all of it. Every behavior of the build is in one directory, not under
node_modules. - It does exactly one thing. No incremental rebuild state, no watch mode, no plugin hooks we'd never use.
- It fails loudly. A strict validator that throws on bad metadata catches let's-rebuild-later problems in CI, where they belong.
The pipeline
The build is a sequence of four steps:
- Load and validate every
content/posts/*.mdfile. - Render markdown to sanitized HTML.
- Assemble pages from templates and write them to
dist/. - Generate the machine-readable files: sitemap, RSS, robots, search index.
1. Strict content validation
Each post starts with frontmatter:
title: "A minimal static site generator"
slug: minimal-static-site-generator
description: "A post about building things simply."
date: 2026-09-04
category: Engineering
tags: ["static-site-generator", "node"]
author: deploy1-teamparseFrontmatter splits on the delimiter, and validatePost enforces the schema. Every field is checked — date format, category against an allowlist, description length, tag character set:
function validatePost({ meta, content, slug, file }) {
assertString(meta, "title", file);
assertDate(meta.date, "date", file);
if (!CATEGORIES.has(meta.category)) {
throw new Error(`${file}: unknown category "${meta.category}"`);
}
// ...
}The key design choice: the build throws on invalid content. A typo in a slug or a description that's too short fails CI, not quietly ships.
2. A constrained Markdown renderer
We wrap marked with a renderer that has three lockdown rules:
- Raw HTML from markdown is discarded (
renderer.html = () => ""). - Link
hrefs are checked against a safe-protocol allowlist; external links getrel="noopener noreferrer". - Code blocks are escaped before insertion.
const renderer = Object.create(marked.renderer);
renderer.html = () => "";
renderer.code = ({ text, lang }) =>
`<pre><code${lang ? ` class="language-${lang}"` : ""}>${escapeHtml(text)}</code></pre>`;Even though our own team writes the content, a renderer that cannot emit raw HTML removes an entire class of bugs pre-emptively.
3. Templates and asset hashing
Pages are assembled by string templates. There's no JSX and no component system — for a site with four page shapes (home, article, category listing, 404), functions like postCard(post) are enough.
Build-time asset hashing keeps caching honest. The CSS gets a content hash:
const hash = crypto.createHash("sha1").update(source).digest("hex").slice(0, 8);Hashed assets are served with an immutable cache header; HTML pages avoid long-lived caching — a short no-cache window with stale-while-revalidate at the edge — so new posts appear immediately.
4. Machine files
Every listing and search-ui feed is generated data, not a plugin:
sitemap.xml— every unique URL, including category and tag archives.rss.xml— publish dates from the validated frontmatter.robots.txt— pointing to the sitemap.search-index.json— title, description, tags and a plain-text excerpt, consumed by a ~150-line client-side search module.
The point isn't the code
The generator is small not because we're clever, but because a blog is small. The discipline that makes it useful is the validation: human-authored content is run through an uncompromising machine check, every build, in CI.
If you're reaching for a static-site framework, ask what a minimal generator gives you first. In many cases the answer is "everything we need, with nothing we have to maintain."
Simple on the outside. Sophisticated underneath — one file at a time.