Why I moved my blog from a CMS to MDX
Local .mdx files beat a hosted CMS for a personal blog. Here's the Next.js setup I shipped in an afternoon — and the trade-offs I weighed first.
For a personal site, the content is small, changes rarely, and only I edit it. That combination makes a hosted CMS mostly overhead: an extra network hop on every build, tokens to rotate, signed image URLs that expire, and an API whose shape you don't control. Moving the body to local MDX removed all of it.
01What actually changed
The data layer, and nothing else. Routes, styling, and every animation stayed exactly where they were — the page components still ask a loader for a post and render the same markup. Only the loader's source moved from an API to disk.
02The loader
Reading a post is now a file read plus frontmatter parsing. No rate limits, no retries, no pagination:
export async function getPostBySlug(slug: string) {
const raw = await fs.readFile(path.join(DIR, `${slug}.mdx`), "utf8");
const { data, content } = matter(raw);
return { ...data, content, toc: buildToc(content) };
}
Because there's no network, the unstable_cache wrapper and the concurrency
limiter both disappeared. Fewer moving parts, faster builds.
Things I deliberately kept
- The table of contents is still derived from the top-level headings.
- Heading ids use the same slug function, so old anchor links keep working.
- The article body stays a server component — zero JS shipped for prose.
03Trade-offs
It isn't free. I lose the browser-based editor and have to write frontmatter by hand. For a blog I touch a few times a month, that's a fine price for owning the content as plain files in the repo.
The best CMS for a one-person blog is a folder of text files and a git history.
Next up: wiring the same treatment to project write-ups.