Adopting Keystatic CMS — Why I Insisted on File-Based
Every post on this blog is an MDX file inside a git repository. Writing a new one meant creating a folder, hand-typing frontmatter, and breaking the build if I got a single field wrong. File management came before writing — and it was becoming my excuse not to write. So I decided to add a CMS. The real question turned out to be which kind.
The candidates, and why they lost
Notion or a headless CMS (Contentful, Sanity, etc.): your content's source of truth moves into someone else's database. This blog doubles as my portfolio, and I didn't want the canonical copy of my writing living outside my repository. If the service shuts down or changes its pricing, your posts become hostages.
A custom admin on a database: buildable, but running a database and an admin panel for one personal blog is the tail wagging the dog.
A file-based CMS (Keystatic, TinaCMS, Decap): content stays in git exactly as it is, with an editing UI layered on top. Not a single line of my existing MDX-reading logic has to change. This is the category I went with, and Keystatic looked like the least friction within it — the schema is defined in TypeScript, and it mounts into the Next.js App Router as a single route.
The setup
Adoption itself was half a day's work:
- Install
@keystatic/coreand@keystatic/next - Define a collection in
keystatic.config.tsmirroring the existing frontmatter schema (title, excerpt, publishedAt, category, tags, coverImage, faqs) - Add the Admin UI route at
/keystaticin the App Router - Exclude
/keystaticfrom the i18n middleware matcher — skip this and the admin gets caught in locale redirects
I kept storage in local mode. With npm run dev running, anything I edit at /keystatic is written straight to local files, and I commit as usual. There's an env-var switch to GitHub mode, but for a one-person blog, local is plenty.
This blog's particular problem: ko/en pairs
Posts here exist as a Korean–English pair (content/posts/ko/<slug> and content/posts/en/<slug>). Keystatic has no concept of "these two entries are translations of the same post." I ended up defining two separate collections, posts-ko and posts-en, and keeping the slugs aligned and the content in sync is still a human job.
What the CMS solved was formatting mistakes, not editorial rules — something I only saw clearly after adopting it.
What changed, what didn't
Changed: frontmatter typos can no longer break the build. The schema is TypeScript, so fields like category are a dropdown. I can write and edit straight from the browser.
Unchanged: keeping ko/en in sync, translating, wrangling images. The parts of writing that actually take time are beyond any CMS.
Honestly, even after adopting it, a doubt lingered for a while: was it right to pass on something as battle-tested as WordPress for this? Whenever it comes back, I return to the original criterion — every post stays as plain text inside my own repository. On that criterion, this is still the right call.
A CMS doesn't make you write. It just removes one excuse not to. For me, that was worth the price of admission.