Blog

No CMS, No BDD, Just Git: Why Simplicity + Validation Wins

6 min read

The Default Assumption

Most people think a blog needs a CMS. I built one without — and it’s more reliable than blogs with full admin dashboards.

The reasoning behind the CMS habit goes like this: a CMS means safety. BDD means confidence. JavaScript means interactivity. So you reach for all three and call it professional.

Look closer and each one trades a small convenience for a permanent cost. A CMS is safety until it’s complexity and vendor lock-in. BDD is confidence until it’s a pile of brittle UI tests you maintain forever. JavaScript is interactivity you don’t need to render text on a page.

For a blog, all three are overhead pretending to be infrastructure.

The Approach I Chose

This blog runs on four ideas, none of them new:

  • Git is the CMS. Posts are Markdown files under version control, with atomic history.
  • Zod validates in the build. Invalid frontmatter fails the build. Period.
  • No BDD, just the build gate. If the schema passes and Lighthouse is green, you’re done.
  • Push to main = live. Cloudflare Pages auto-deploys the static output.

No database. No admin login. No “publish” button. The code is the law, and the law is enforced at build time.

Why This Works

1. Git is literally a better CMS

Every CMS reinvents a worse version of what Git already does. Drafts? That’s a branch. Edit history? That’s the log. Undo a bad change? That’s a revert. Who changed this line and why? That’s blame plus a commit message.

A CMS stores your content in a database you have to back up, migrate, and pray never corrupts. Git stores it in plain files you can read with any text editor, on any machine, offline, forever. The source of truth is a file you own — not a row in someone else’s black box.

When I want to write, I open a file. When I want to publish, I commit. The “content management” was solved decades ago; I just stopped paying a CMS to do it worse.

2. Zod validation beats tests

Here’s the schema every post on this site must satisfy:

schema: z.object({
  title: z.string().max(60),
  description: z.string().max(160),
  pubDate: z.coerce.date(),
  tags: z.array(z.string()).min(1),
  draft: z.boolean().default(false),
  // ...
})

That’s not documentation. It’s a gate. A title over 60 characters, a missing date, a post with zero tags — any of these and npm run build fails before anything ships.

Tests catch bugs after you write them. A schema prevents the bug from existing. I can’t publish a malformed post, because malformed posts don’t compile. That’s a stronger guarantee than a green test suite, and I didn’t have to write or maintain a single test to get it.

3. Static HTML beats dynamic rendering

The blog is built once and served as flat HTML. There’s no server rendering a page on each request, no runtime to throw an exception, no cold start. HTML can’t crash.

This is also why Lighthouse scores stay high without effort — there’s simply nothing to slow it down. SEO metadata is baked into the file at build time, not assembled on the fly. The fastest request is the one that does no work, and a static file does no work.

4. The deploy pipeline fits in your head

git push main

GitHub webhook

npm run build   (Zod validates · sitemap generated · RSS built)

Cloudflare Pages deploys

Live

No manual steps. No staging dance. No review gate I have to remember to click through. One push either succeeds entirely or fails entirely — there’s no half-published state where the post exists but the listing forgot about it.

What You Lose (and Don’t Miss)

Be honest about the tradeoffs, because there are some:

  • Admin UI — You don’t click buttons to write. You type in an editor. If you’re a developer, this is a feature, not a loss.
  • Real-time previewnpm run dev is the preview, and it hot-reloads.
  • Scheduling — I publish when a post is ready, not on a calendar.
  • Comments — A separate concern, solved better elsewhere than by bolting a comment database onto a static site.

None of these have cost me anything. They were conveniences I assumed I needed before I checked whether I did.

What You Gain

  • Version control — A full audit trail of every word, for free.
  • Offline writing — Git works on a plane. A CMS doesn’t.
  • Portability — Markdown is future-proof. Try exporting cleanly from a CMS sometime.
  • Speed — Zero runtime overhead, by construction.
  • Confidence — Invalid content cannot ship, by construction.

Why BDD Isn’t Needed Here

A typical BDD scenario reads: “When I click publish, the post appears on the listing.” The problem is you’re testing the UI, not the logic — and the test can pass while production quietly differs, because the test runs against a mock and prod runs against reality.

My version of that guarantee is structural: if the build passes, the post is valid and live. There’s no gap between the test environment and production because there’s only one tool — Astro builds the same output locally and in CI. Validation is a schema, not an assertion I hoped to write correctly. The build gate is the test, and it’s impossible to forget to run.

The Real Win

What I ended up with is a blog that:

  • Can’t have broken frontmatter (Zod won’t allow it)
  • Can’t be slow (it’s static HTML)
  • Can’t lose history (it’s Git)
  • Can’t get locked into a vendor (it’s Markdown)
  • Deploys atomically (one push, all or nothing)

All of that without a CMS, without BDD, and without a line of JavaScript.

The catch — and there is one — is that you need to be comfortable with Git, with Markdown, and with running npm run build before you push. If you’re already a developer, that’s not a learning curve. It’s just less than what a CMS asked of you in the first place.

Simplicity isn’t the absence of capability. It’s refusing to pay for capability you don’t use.


This blog is built slice by slice with Spec-Driven Development. Curious how the whole system came together? Read “How I Built This Portfolio with SDD,” or browse the rest at /blog.