On September 28, 2026, EmDash 1.0 was officially released.
EmDash is an open-source CMS developed by Cloudflare, built on Astro, with its admin interface embedded directly within your Astro site, running on Cloudflare Workers + D1 + R2.
You could say it's roughly the Astro version of WordPress.
In a previous post, our internal blog "Yuru-log" was migrated from microCMS, but it had remained on version 0.22 since June, so we took this opportunity to upgrade all the way to the latest 1.2.0 in one go.
The highlights of 1.0 are improved stability and upgrade safety, sandboxed plugins, and MCP servers for agents.
To cut to the chase, the migration was completed in a day, but there were many pitfalls.
If you start thinking "1.0 should be stable," you'll run into issues not with EmDash itself but with surrounding dependencies.
Context: 0.22 → 1.2 had to be upgraded manually
Version 1.2 includes upgrade-emdash, an official upgrade CLI.
It updates dependencies and generates a site-specific UPGRADE.md, but it only supports 0.35 and later, so it couldn't be used from 0.22.
Between 0.22 and 1.2, there are 47 database migrations, with several breaking changes along the way.
It wasn't something you could just npm install emdash@latest and call it done.
1. Back up and replicate the production database locally
npx wrangler d1 export <database-name> --remote --output=backups/pre-1.2.sql
We run this through the local D1 and test migrations with the same data as production.
2. Update dependencies
Package | Before migration | After migration |
|---|---|---|
emdash / @emdash-cms/cloudflare / @emdash-cms/admin | 0.22.0 | 1.2.0 |
@emdash-cms/plugin-forms | 0.1.1 | 0.2.9 |
astro | 6.1.2 | 6.4.8 |
wrangler | 4.80 | 4.148 |
wrangler needed to be bumped due to emdash 1.2's peer requirement (4.99 or higher), so I also removed the temporary workaround from the 0.22 era at this timing. @astrojs/cloudflare met the peer requirements, so it was left as is at 13.1.6 at this point. This becomes the cause of the next trap.
- patch-package for OAuth route: Applied to work around a 0.22 bug. Already fixed in the main body.
- CSS injection into the admin panel: The 0.22 admin's CSS was for UI library kumo 2.4, but the lockfile was pulling 2.6, causing login and publish emphasis buttons to become invisible (transparent background with white text). This is fixed in the main body in 1.2.
3. Dev won't start (trap: miniflare version mismatch)
After bumping dependencies and running npm run dev, the startup failure was caused by miniflare version mismatch.
wrangler 4.148 uses miniflare 5 and updates the _cf_ALARM table to 3 columns in the local D1 state (under .wrangler/state/v3/).
Meanwhile,@astrojs/cloudflare 13.1.6 bundles miniflare 4, and the column count mismatch prevents it from reading the local state.
The fix is to also bump the pinned @astrojs/cloudflare to align miniflare's version with wrangler. Just remember: "align wrangler and adapter miniflare versions," and next time you'll get it right the first time.
As of version 0.22, the emdash dev command was deprecated, so the dev script was reverted to astro dev. Migration and type generation now run when astro dev starts.
4. Staging menu disappeared (gotcha: menus in seed.json)
After dev started, 90 migrations passed, and staging was pushed to the admin panel, the Japanese global navigation was empty.
The cause is the menus and widgetAreas in seed/seed.json.
EmDash's seed process deletes all items and then recreates them as empty when a menu or widget area with the same name exists.
The seed.json had menu definitions with "items": [] left as a template, which is why the menu was being reset every time the seed ran.
The fix was to delete the menus and widgetAreas keys entirely. Since
menu contents are managed in the admin panel, there's no reason to keep them in the seed.
5. English article links become /en/posts/en/slug (gotcha: locale prefix in entry.id)
This is a spec change in version 1.2. In multilingual sites, getEmDashCollection() now returns entry.id with a locale prefix. For an English article, that's en/my-post.
Yuru Log was building article URLs from entry.id, so English article links became /en/posts/en/my-post, doubled up. We replaced all URL-building locations—
lists, pickups, search results, RSS, and canonical—with entry.data.slug. Since the type of entry.data.slug is string | null, you also need to update the receiving type.
It helps to remember the roles: entry.id is the key, entry.data.slug is the URL, and entry.data.id is the database ULID (for API calls).
6. Errors every minute (gotcha: Cron Trigger and Worker entry)
In 1.x, the Worker's scheduled() handler drives scheduled post publishing and periodic plugin processing.
Since Cloudflare has no internal timer, without a Cron Trigger, scheduled posts won't publish at their scheduled time.
For this reason, I added a Cron Trigger to wrangler.jsonc.
"triggers": { "crons": ["* * * * *"] }Then every minute I got "Handler does not export a scheduled() function".
The Worker entry wasn't exporting scheduled().
The fix was to replace src/worker.ts with the official entry point.
export { default, PluginBridge } from "@emdash-cms/cloudflare/worker";Now I have fetch and scheduled along with PluginBridge for sandbox-type plugins.
Trying it out for real
Why upgrading to 1.2 was worth it.
- The editor has been completely redesigned and is much more intuitive. The slash menu is now grouped, and the toolbar follows your cursor. It works on mobile too.
- Video blocks are now built-in. I had been handling video embeds with a custom plugin, so I'd like to replace that eventually.
- All the workarounds from the 0.22 era are gone. No more patch-package or CSS injection.
Things to watch out for.
- The pace from 1.0 to 1.2 was fast—just 10 days. We'll likely need a workflow that checks staging for each minor version release going forward.
Summary
- 0.22 → 1.2 is a manual migration.
The upgrade-emdashCLI only supports 0.35 and later. - Align wrangler and adapter miniflare versions → dev fails to start if they don't match
- Don't leave empty templates for
menus/widgetAreasin seed.json → they disappear with each seed - Multilingual sites have locale prefix in
entry.id→ URL comes fromentry.data.slug - To add Cron Trigger, set Worker entry to
@emdash-cms/cloudflare/worker
Each of these is just a few lines to fix once you understand the issue, but investigating one by one takes far more time than you'd expect.
That said, being able to remove all the patches and CSS workarounds we had applied in 0.22 shows that minor bugs have been steadily squashed leading up to the official release.
Six months ago it felt experimental to swap it in, but after reaching 1.0, if you factor in the maintenance effort for updates, EmDash has become a CMS worth seriously considering for production use.
A "master of technique" who jumped from DTP into the web world and, before he knew it, mastered markup, frontend, direction, and accessibility. Active across multiple domains since Liberogic's early days, he's now a walking encyclopedia within the company. Recently, he's been diving deep into prompt-driven efficiency optimization, wondering "Can we rely more on AI for accessibility compliance?" Both his technology and thinking continue to evolve.
Ayumu Futamata
IAAP Certified Web Accessibility Specialist (WAS) / Markup Engineer / Frontend Engineer / Web Director