2026年9月28日、EmDashの1.0が正式リリースされました。
EmDashはCloudflareが開発しているAstroベースのオープンソースCMSで、管理画面ごとAstroサイトの中に組み込まれ、Cloudflare Workers + D1 + R2で動きます。
WordPressのAstro版と言うとだいたい合っています。
以前の記事でmicroCMSから載せ替えた社内ブログ「ゆるろぐ」は、6月に上げた0.22のまま止まっていたので、このタイミングで最新の1.2.0まで一気に上げてみました。
1.0の目玉は、安定性とアップグレードの安全性、サンドボックスで動くプラグイン、エージェント向けのMCPサーバーあたりです。
結論から言うと移行は1日で終わりましたが、罠は多かったです。
「1.0になったから安定しているだろう」と軽い気持ちで始めるとEmDash本体ではなく周辺の依存関係で詰まります。
前提:0.22 → 1.2は手動で上げるしかなかった
1.2にはupgrade-emdashという公式のアップグレードCLIが入りました。
依存関係を更新してサイト固有のUPGRADE.mdを生成してくれますが、対応しているのは0.35以降なため0.22からだと使えませんでした。
0.22から1.2の間にはデータベースのマイグレーションが47本挟まっていて、そこそこ破壊的な変更もいくつかあります。
「とりあえずnpm install emdash@latest」で済む話ではなかったです。
1. バックアップを取って、ローカルに本番DBを複製
npx wrangler d1 export <database-name> --remote --output=backups/pre-1.2.sql
これをローカルのD1に流して、本番と同じデータでマイグレーションを試します。
2. 依存関係を更新
パッケージ | 移行前 | 移行後 |
|---|---|---|
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はemdash 1.2のpeer要件(4.99以上)で上げる必要があったため、このタイミングで0.22時代の応急処置も外しました。@astrojs/cloudflareはpeer要件を満たしていたので、この時点では13.1.6のまま据え置いています。これが次の罠の原因になります。
- OAuthルートへのpatch-package:0.22のバグ回避で当てていたもの。本体で修正済み
- 管理画面のCSS差し込み:0.22のadminはUIライブラリkumo 2.4向けのCSSなのにlockfileが2.6を引いていて、ログインや公開の強調ボタンが「透明背景に白文字」で見えなくなっていました。1.2では本体側で直っています
3. devが起動しない(罠:miniflareの世代ずれ)
依存を上げてnpm run devすると起動しない原因はminiflareの世代ずれでした。
wrangler 4.148はminiflare 5を使っていてローカルのD1状態(.wrangler/state/v3/配下)の_cf_ALARMテーブルを3列に更新します。
一方@astrojs/cloudflare 13.1.6が同梱しているminiflareは4で列数の不一致でローカルの状態を読めません。
対処は据え置いていた@astrojs/cloudflareも上げてminiflareの世代をwranglerに揃えること。「wranglerとアダプタのminiflare世代は揃える」とだけ覚えておけば次回は一発です。
あわせて0.22にあったemdash devコマンドは廃止されたので、devスクリプトはastro devに戻しました。マイグレーションと型生成はastro devの起動時に走ります。
4. stagingのメニューが消えた(罠:seed.jsonのmenus)
devが起動してマイグレーション90本が通りstagingに上げて管理画面を開いたら、日本語のグローバルナビが空になっていました。
原因はseed/seed.jsonのmenusとwidgetAreasです。
EmDashのシード処理は同名のメニューやウィジェットエリアがあると項目を全削除してから空で再作成します。
seed.jsonには雛形として"items": []のメニュー定義が残っていたのでシードが走るたびにメニューが初期化されていたわけです。
対処はmenusとwidgetAreasのキーごと削除。
メニューの中身は管理画面で管理するものなのでseedに持たせる理由はありません。
5. 英語記事のリンクが /en/posts/en/slug になる(罠:entry.idのロケール接頭辞)
これは1.2の仕様変更です。
多言語サイトでは、getEmDashCollection()が返すentry.idにロケールの接頭辞が付くようになりました。英語記事ならen/my-postです。
ゆるろぐは記事URLをentry.idから組み立てていたので、英語記事のリンクが/en/posts/en/my-postと二重になりました。
一覧・ピックアップ・検索結果・RSS・canonicalとURLを組み立てている箇所を全部entry.data.slugに置き換えています。entry.data.slugの型はstring | nullなので受け取る側の型も合わせて直す必要があります。
entry.idはキー・entry.data.slugがURL・entry.data.idがDBのULID(API呼び出し用)と役割を分けて覚えておくとよいです。
6. 毎分エラーが出る(罠:Cron TriggerとWorkerエントリ)
1.xでは予約投稿の公開やプラグインの定期処理を、Workerのscheduled()ハンドラが駆動します。
Cloudflareでは内部タイマーがないのでCron Triggerがないと予約投稿が公開時刻になっても出ません。
そのためwrangler.jsoncにCron Triggerを追加しました。
"triggers": { "crons": ["* * * * *"] }すると今度は毎分 "Handler does not export a scheduled() function" が出ます。
Workerのエントリがscheduled()をexportしていないからです。
対処はsrc/worker.tsを公式のエントリに差し替えること。
export { default, PluginBridge } from "@emdash-cms/cloudflare/worker";これでfetchとscheduledに加えてサンドボックス型プラグイン用のPluginBridgeも揃います。
実際使ってみて
1.2に上げて良かった点。
- エディタが刷新されて使いやすい。スラッシュメニューがグループ化され、ツールバーが追従するようになりました。モバイルでも使えます
- 動画ブロックが標準で入った。自作プラグインで動画埋め込みをしていたので、そのうち置き換えたいところ
- 0.22時代の応急処置が全部外せた。 patch-packageもCSS差し込みもなくなりました
気になる点。
- 1.0から1.2までが10日で、動きが速い。しばらくはマイナーバージョンごとにstagingで確認する運用が必要そうです
まとめ
- 0.22 → 1.2は手動移行
upgrade-emdashCLIは0.35以降が対象 - wranglerとアダプタのminiflare世代を揃える→揃っていないとdevが起動しない
- seed.jsonに
menus/widgetAreasの空の雛形を残さない→シードのたびに消える - 多言語サイトは
entry.idにロケール接頭辞が付く→URLはentry.data.slugから - Cron Triggerを足すならWorkerエントリを
@emdash-cms/cloudflare/workerにする
どれも分かってしまえば数行の修正ですが一つずつ調べながら進むと思った以上に時間を取られます。
とはいえ0.22のときに当てていたパッチやCSSの応急処置が全部外せたのは正式リリースに向けて細かいバグが潰されてきた証拠でもあります。
半年前は「実験的に載せ替えてみた」という温度感でしたが1.0を経て、更新に追従する手間さえ見込めば、実運用の選択肢として十分検討できるCMSになってきたと思います。
DTPからWebの世界へ飛び込み、気づけばマークアップもフロントエンドもディレクションもアクセシビリティもこなす"技の仙人"。リベロジック創業期からマルチに活躍し、今や社内の生き字引的存在。最近は「アクセシビリティ対応、もっとAIに頼れないかな?」と、プロンプトを駆使した効率化の探究にハマり中。技術も思考も、まだまだ進化中
二俣 歩
IAAP認定ウェブアクセシビリティスペシャリスト(WAS) / マークアップエンジニア / フロントエンドエンジニア / ウェブディレクター