Why I migrated from Sanity to EmDash CMS
// I moved xuniti.com from Sanity to EmDash CMS over a weekend. Here's what pushed me off, what I lost, and why I'd do it again.
Sanity served me well for two years. Then I rebuilt xuniti.com on EmDash over a weekend and haven't looked back.
This isn't a Sanity hit piece. It's a real CMS with real strengths. But my needs shifted, the bill kept climbing, and the structured-content overhead stopped making sense for a one-person studio. Here's what actually pushed me out.
What Sanity got right
Credit where it's due. Sanity's content lake is a clever idea — query your content like a database, get real-time updates, structured to the core. For agencies wrangling content across dozens of clients, it's hard to beat.
The Studio is genuinely good. Custom input components, references, validation, presentation tools — all of it works. I shipped my first version of xuniti.com on Sanity because I could prototype the schema in an hour.
If you're a team of four building marketing sites for Fortune 500s, Sanity is probably the right answer.
Where it stopped working
I'm not a team of four. I'm one person shipping indie tools and writing about them on the side.
The Studio became overhead. Every time I wanted a new field I edited a schema file, redeployed Studio, then went back to the content. The "structured content" pitch — content as data, queryable everywhere — turned into "I have to think about GROQ for a 500-word blog post."
Then the bill. The free tier is generous until it isn't. My API requests crept up as traffic grew. Bandwidth on the asset CDN added up. The price wasn't outrageous, but I was paying SaaS rent to host markdown.
The bigger thing: my content lived on someone else's servers. Sanity is fine, but "fine" doesn't replace "I can grep my own posts." When I write about open-source tools first, running a closed-source hosted CMS started feeling like a contradiction.
Why EmDash
EmDash runs locally. The admin UI sits at /_emdash/admin on my own site. Content lives in my SQLite database. The whole thing is open source.
That's the entire pitch. Everything else — markdown-native editing, no schema redeploys, no separate Studio instance — follows from those facts.
It's not as featureful as Sanity. There's no content lake, no real-time collab, no fancy presentation mode. I write blog posts and tutorials, not branching content for ten regional sites. The feature gap doesn't matter for what I do.
The Astro integration is also why I picked it. xuniti.com runs on Astro 6 with the Node SSR adapter, and EmDash plugs into that directly. No separate deployment, no second domain, no CORS headaches.
The migration
Two days, end to end.
Day one was export. Sanity's CLI dumps everything to NDJSON. I wrote a small Node script that read the export, mapped Sanity's portable text to markdown, and produced one .md file per post with frontmatter. The block-to-markdown step is where most of my time went — portable text has nuances around marks and nested annotations that don't map cleanly:
// rough shape of the converter
function blockToMarkdown(block) {
if (block._type !== 'block') return handleCustom(block);
return block.children
.map(child => applyMarks(child.text, child.marks, block.markDefs))
.join('');
} Day two was import. I dropped the markdown files into EmDash's admin, post by post. Could have scripted it, but with 18 posts each paste was a chance to re-read and fix old typos. Worth it.
Images were the boring part. Sanity hosts assets on its CDN. Another script downloaded every referenced image and rewrote the markdown to point at local paths under /public. About an hour.
Redirects were the part I almost forgot. Every old /blog/{slug} URL needed to map to /posts/{slug}. I added an Astro middleware that handles the redirect with a 301. Not exciting, but skipping this kills your SEO.
Tradeoffs I'm honest about
I lost real-time collab. I work alone, so this is irrelevant to me. If you have editors, this matters.
I lost the asset CDN. Images now serve from the same origin as the site. For a low-traffic indie blog this is fine. At hundreds of thousands of pageviews you'll want a CDN in front.
I lost Sanity's preview mode. EmDash has its own, but it's simpler — no branching content, no scheduled previews against staging data.
I gained ownership. Every post, every image, every config file is in my repo or on my disk. I can grep the whole CMS. I can back it up with cp. If EmDash disappears tomorrow, I still have markdown files — readable forever.
I also gained speed. The CMS lives in the same process as the site. No API roundtrip to fetch posts, no rate limit to think about, no separate dashboard to log into.
What's next
I'm writing a script to auto-generate frontmatter from a prompt — title, description, tags, slug — so drafts move faster. I'll probably open-source it once it stops being janky.
If you're on Sanity and it's working for you, stay. If you're paying SaaS rent for a personal site and the structured-content overhead has started to grate, EmDash is worth a weekend.
I'll write a proper migration tutorial once I've done one more site and the process is repeatable. Until then, the code lives in my head and a few half-finished scripts.