JDM
← All posts
6 min read

Why I write this blog in MDX instead of using a CMS

Zero monthly cost, same git workflow as code, type-checked frontmatter, and no preview environment juggling. Trade-offs are real but acceptable for a solo site.

mdxblogtoolingsolo-founder

Every time someone asks me for CMS recommendations, I have to stop myself from saying "just use MDX." That advice does not work for everyone. But for a technical solo blog like this one, a CMS adds complexity I do not need.

I write posts as .mdx files in /content/blog/, parse frontmatter with gray-matter, render with next-mdx-remote/rsc, and extend Markdown with remark-gfm. The whole pipeline is about 150 lines in lib/blog.ts. It took me an afternoon to wire up and I have not touched it since.

Let me walk through why this setup works for me, where it falls short, and when I would switch.

Zero monthly cost, zero ops

Sanity starts at $0 for a free tier but you hit limits on API requests or team seats. Contentful's free tier caps content entries. Strapi means you run a database and a Node service. Even the cheap plans add up over a year.

My blog costs nothing beyond the VPS I already run for other sites. I deploy via Coolify. The build pulls the repo, runs next build, and spits out static HTML. No database, no API keys, no rate limits.

If I had to pay $20 a month for a CMS, I would not. The blog is a side project, not a revenue center. Every dollar of recurring cost is a tax on writing.

One editor, one workflow

I write code in VS Code. I write blog posts in VS Code. Same editor, same keybindings, same git workflow. I branch, write, commit, push. The deploy hook on Coolify picks it up.

There is no "preview environment" button. I run npm run dev locally and the post renders exactly as it will in production. The MDX components are the same. The CSS is the same. What I see is what ships.

When I need to fix a typo, I open the file, edit, commit. No logging into a headless CMS dashboard, no navigating nested fields, no "did I save the draft?" anxiety.

Type-checked frontmatter

Every post starts with a YAML block:

---
title: "Why I write this blog in MDX instead of using a CMS"
excerpt: "Zero monthly cost, same git workflow as code..."
date: 2026-04-08
tags: ["mdx", "blog", "tooling", "solo-founder"]
ogImage: "/images/blog/mdx-over-cms.webp"
draft: false
---

I define a TypeScript type for frontmatter in lib/blog.ts. If I forget a required field or use the wrong tag type, the build fails. No runtime surprises.

Compare that to a CMS where fields are loosely typed or require custom validators. TypeScript catches my mistakes before I commit. I cannot count how many times it has saved me from publishing a post with a missing date or a misspelled tag.

Backups are git

I never worry about losing content. Every post is a text file in a git repo. I can roll back to any version, diff changes, see who wrote what (me, always me). If Coolify goes down, my content is still on GitHub.

With a CMS, you trust their export tool or their backup schedule. I have seen enough "export to JSON" panic threads on Reddit to know I do not want that stress.

The trade-offs I accept

No WYSIWYG. I write raw Markdown and MDX. That means I type ## for headings and ** for bold. For code blocks I wrap in triple backticks. I am comfortable with that. If you edit prose all day and hate Markdown, this setup is not for you.

No multi-author workflow. I am the only writer. If I ever add a second author, I will need to think about git conflicts, review processes, and maybe a CMS. But for now, one git history, one voice.

Image management is manual. I drop hero images into /public/images/blog/ and link by path. If I rename an image, I have to update every post that references it. A CMS would handle asset management with a media library. I accept this because most of my visuals are generated by nanobanana and versioned alongside the post anyway. I rarely reuse an image across posts.

When I would switch to a CMS

The day I have more than one author shipping content weekly, I will evaluate a CMS. Git-based workflows get messy with multiple people editing the same files. PR reviews work for code but are awkward for prose edits.

If marketing wants to A/B test blog post titles without redeploying, a CMS wins. I could hack something with query parameters and client-side logic, but it would be fragile. A headless CMS makes that trivial.

If I need an editorial calendar with approval workflows, I would reach for Sanity or Strapi. GitHub PRs can approximate that, but the UI is not designed for non-technical editors. I would rather give a marketing person a clean dashboard than teach them git.

The generation pipeline

I do not write every post from scratch. I have a script called tools/gen-blog-deepseek.mjs that takes a topic and a brief, calls DeepSeek V3, parses the MDX output, and writes the file to disk.

The workflow is: write a tight brief, run the script, fact-check the output, commit. LLMs invent specific details, so I always verify. But for structure and first drafts, it saves me time.

The script respects my voice rules because the brief enforces them. No em dashes, no marketing slop, first person, concrete examples. If the brief is loose, the output is useless. Tight briefs produce usable drafts.

The tooling stack

The core dependencies are small:

{
  "gray-matter": "^4.0.3",
  "next-mdx-remote": "^6.0.0",
  "remark-gfm": "^4.0.1"
}

gray-matter parses the YAML frontmatter. next-mdx-remote/rsc renders MDX as a React Server Component. remark-gfm adds GitHub Flavored Markdown: tables, footnotes, task lists.

I wire them together in lib/blog.ts with custom MDX components for h2, h3, p, ul, li, blockquote, code, and pre. That is where I apply consistent styling and add things like copy buttons for code blocks.

The entire blog system is about 150 lines of glue code. I could have used a CMS and spent the same amount of time configuring it. But then I would be locked into their API, their pricing, and their upgrade cycle.

What I learned from migrating off Vercel

When I moved this site from Vercel to Coolify, the blog was the easiest part. The MDX files are just data in the repo. I did not need to export anything, reconfigure webhooks, or worry about API compatibility. I just pointed the build command at the same Next.js project and it worked.

That experience is documented in my Vercel-to-Coolify migration post. The blog's portability was a side effect of choosing files over a CMS. I did not plan for it, but it paid off.

The honest takeaway

MDX is not the right tool for every blog. If you have a team of non-technical editors, a CMS is better. If you need rich media management or A/B testing, a CMS is better. If you want to write in a browser and see a live preview, a CMS is better.

But for a solo technical blog, MDX is simpler, cheaper, and more portable. It removes the operational overhead of a CMS without removing any feature I actually use. The trade-offs are real, but I know them and I accept them.

I also ran into a gotcha with GitHub deploy keys during the migration. If you manage multiple repos from one VPS, check my post on GitHub deploy keys uniqueness. It saved me an hour of debugging.

For now, I keep writing in VS Code, committing to git, and letting Coolify build the static output. No CMS dashboard, no monthly bill, no lock-in. Just files.