I Made My Blog Publish Itself From Obsidian
Back in Building This Site, I said I'd "eventually add some sort of automation from Obsidian (my writing tool of choice) to this site." Here we go!
I write posts in Obsidian like any other note, set one field in the metadata to published, and a few minutes later they're live. I don't have to copy-pasta, remember the deploy commands, or make sure formatting matches. This is documentation on how I (Claude) wired it up and more honestly, everything that didn't work along the way.
What I wanted
Three things:
- Write in Obsidian. It's where my notes already live. I didn't want a separate blog-writing workflow.
- A publish switch. A note should stay private until I decide it's ready. Drafts could stay drafts until I thought it was ready and could live in the same folder as published posts.
- No deploys to think about. Hit publish, walk away, it's on the site.
How it works
The blog is just markdown files in the site's repo. The trick is getting my Obsidian vault into that repo automatically and only the ones I've marked published in the folder so all of my notes aren't publicly viewable.
Obsidian (I write, set status: published)
│
│ Obsidian Git pushes my vault
▼
private vault repo
│
│ GitHub Action, every ~15 min: grab published notes, tidy them up,
│ copy into the blog repo
▼
blog repo ──► Vercel sees the commit ──► live site
My whole vault lives in a private repo (using the Obsidian Git plugin). A GitHub Action pulls it, keeps only the notes marked published, converts a few Obsidian-isms to plain markdown, and commits them into the blog's repo. Vercel notices the commit and redeploys. That's it.
The publish switch
Every post starts with a little block of frontmatter. The one that matters:
---
title: "My Post"
date: 2026-10-14
status: published # draft (or missing) keeps it private
---
The site only renders status: published. A future date even lets me schedule: the post stays hidden until then and goes live the next time the sync runs after that date. Unpublishing is just flipping it back to draft.
A peek at the machine
The sync is a GitHub Action on a timer, plus a small script that does the actual copying and cleanup:
on:
schedule:
- cron: "*/15 * * * *" # check every ~15 minutes
workflow_dispatch: {} # ...or a "Run workflow" button when I'm impatient
The script reads my vault's blog/ folder, skips anything not published, and rewrites the Obsidian-only syntax so it renders correctly on the web. A wikilink to another published post turns into a real link (that's how the one up top points back to the last post); links to anything unpublished flatten to plain text. Image embeds get copied out and relinked, and callouts become blockquotes. Standard markdown passes straight through.
The part where I broke everything
It did not work on the first try. Or the second. A tour of my mistakes, so you can skip them:
- Vercel rejected every deploy. My commits were authored under one email; my Vercel account knew me by another. Vercel quietly blocked the deploys until the commit email matched. One
git configline fixed it. - I put the secrets in the wrong house. The sync needs a token to read my private vault. I confidently added it to Vercel's environment variables, which the GitHub Action cannot see. They go in GitHub repo Secrets. (Also: Secrets, not Variables. Different tab, same trap.)
- My Actions tab was empty. My repo's default branch wasn't the branch my workflow lived on, and GitHub only reads workflows from the default branch. The whole thing was invisible until I switched the default.
Every one of these was a five-minute fix hiding behind a confusing error. Which is most of software, really.
The payoff
I wrote this post in Obsidian. I didn't touch the terminal, didn't open the repo, didn't run a single command. I set status: published, and the pipeline I just described carried it the rest of the way to the page you're reading.
Set it and forget it. The best kind of automation is the kind you stop noticing.