How I Built a CMS Using Obsidian
My writing tool is my CMS. Here's how that happened.
How I Built a CMS Using Obsidian
1. The Old Workflow
I write posts for this blog in my note-taking tool, Obsidian. The writing experience there is just better compared to most alternatives out there.
Previously the workflow was:
Ideate in my notebook → write the draft in Obsidian → copy the final draft into the blog folder of my website → push to Git → trigger deploy on Netlify.
During one copy-paste session, prepping to publish a post, I got to thinking.
I can automate this.
2. What I Mean by CMS
So when I say CMS, I mean it in the sense that it’s a system that lets you write and publish without touching code.
WordPress is a CMS. Contentful, Sanity, even Notion can be used as a CMS.
All of these conventional alternatives add a vendor, a login, a monthly subscription, and a proprietary editor.
It requires me to change how I like to create just to have a publishing platform.
I already have an editor I like. I just built the pipe connecting it directly to my website.
3. Did I Just Build Myself a CMS?
In that process I just had a thought.
“Did I just build myself a CMS using Obsidian?”
Yeah, I did.
What the stack is:
- Obsidian — editor & draft management
- GitHub — the “database”, versioned and diffable
- n8n — publishing engine, self-hosted
- Cloudinary — image hosting
- Astro on Netlify — the frontend that builds from the content
Now: Notebook → Obsidian → Published post.
4. The Publishing Flow
Here’s what actually happens when I write a post now.
I start a new note in 06 - Blog/, built from a template that already lays out the frontmatter fields I need: title, slug, excerpt, tag, and a publish switch that defaults to false. I write the post the same way I write everything else in this vault. No separate app, no separate mental mode.
When it’s ready, I flip publish to true and save. That’s the entire action on my end.
An n8n workflow polls that folder every 30 minutes. It reads the frontmatter on every note inside, and if it finds publish: true and a published_status that isn’t already done, it picks the post up. It converts any [wikilinks](/writing/wikilinks) into proper /writing/slug URLs, checks GitHub for whether a post with that slug already exists (fetching the file’s SHA first if so, which GitHub’s API insists on for any update), and pushes the markdown into the blog repo at the right nested path. Once that succeeds, it writes published_status: done back into the vault note itself, so the same post doesn’t go out twice on the next poll. Then it fires a Netlify deploy hook, and the site rebuilds.
I never open a terminal. I never leave Obsidian. The whole loop lives inside one flip of one field.
5. The Honest Build Story
I want to be upfront that none of this worked on the first try.
The workflow ran end-to-end successfully for the first time on execution 1717. I know that because n8n logs every run, and I went back and checked. The commit hash from that first successful push is still sitting in GitHub.
A few of the bugs are worth naming, not as a technical teardown, but because they’re the kind of thing that makes a personal system feel like a real engineering problem instead of a weekend toy. The repo path was nested a directory deeper than I’d accounted for, and GitHub doesn’t surface that kind of mismatch clearly, it just fails silently. I had two Bearer tokens in n8n that looked identical in the credentials list, and I kept grabbing the wrong one. The newer JS task runner in n8n rejects code nodes set to run once per item if the code returns an array, which is exactly the kind of thing you only learn by hitting it. And GitHub’s Contents API requires a SHA lookup before it’ll let you update a file, silently, with no obvious error pointing you there.
None of that is dramatic. It’s just what it costs to actually ship something that runs unattended. The point isn’t that this was hard in some impressive way. The point is that “just build a small automation” always turns out to be execution 1717, not execution 1.
6. What Writing in It Actually Feels Like
The part that’s easy to undersell is what day-to-day writing feels like once the pipe exists.
Drafts live in the same vault as my thinking notes, so I can link a blog post directly to the note where the idea first showed up, and back again. The template pre-fills the entire frontmatter schema, so I never have to remember what fields the blog expects. Obsidian is offline-first and doesn’t ask me to log into anything, which after years of hosted CMS rich text editors is a genuinely nicer place to write in.
The only “publishing UI” I have is a single frontmatter field. There’s no dashboard, no preview button, no separate publish screen.
Before I flip that field, I run the draft through a set of style rules I’ve built up, mostly to catch the kind of generic, over-tidy phrasing that AI-assisted drafting tends to produce. That’s a separate editorial layer sitting on top of the CMS, not part of it. The CMS doesn’t need to provide that. It just needs to get out of the way once the writing is done.
7. What This Doesn’t Have
I don’t want to oversell this, so here’s what’s missing.
There’s no visual content dashboard. A Dataview query inside Obsidian is a rough substitute, but it’s not the same as a CMS’s post-listing screen. There’s no scheduling either. If I flip publish: true today, it goes out on the next 30-minute poll, not on a date I pick in advance.
The maintenance surface is also real. If Astro’s content schema changes, or an Obsidian plugin update shifts how frontmatter gets parsed, the pipe can break in ways a hosted CMS would have abstracted away from me.
My honest take is that none of that is actually the bottleneck. The bottleneck on a personal blog was never the publishing UX. It’s whether you write the post at all. This system just removes every excuse I used to have for not doing that.
8. Should You Build This?
I’m not trying to sell you on this exact stack.
The real question is whether you already live in a markdown-native tool. If you do, the cost of a pipeline like this is lower than it looks from the outside, it’s mostly a weekend and a willingness to debug n8n’s error messages. If you don’t already work that way, a hosted CMS is probably the right call, and building this would just be solving a problem you don’t actually have.