Cloudflare が公開しているオープンソースの CMS「EmDash」で、ブログを Cloudflare Workers 上に立ち上げました。そのときの手順と、途中で気づいた点を記録しておきます。
この記事の画像には、個人情報が写らないようにしています。ブラウザの画面はローカル環境でダミーデータを使って撮り、ターミナルの画面はメールアドレスやアカウント ID をダミーの値に置き換えました。やり方は記事の最後に書いています。
EmDash とは
EmDash は、Cloudflare が 2026 年 4 月に発表したオープンソースのフルスタック CMS です。「WordPress の現代的な後継」を掲げています。
Astro をベースにしていて、管理画面(/_emdash/admin)も含めて 1 つの Astro プロジェクトに収まっています。Cloudflare では、アプリ本体を Workers、データベースを D1、メディアの保存先を R2 で動かします。Node.js と SQLite の組み合わせでも動きます。
Sandboxed Plugins を有効にすると、プラグインは隔離された Worker(Worker Loader)の中で動きます。WordPress では 1 つのプラグインの不具合がサイト全体に及ぶことがありますが、その心配が小さくなります。
今回の構成
役割 | サービス | バインディング名 |
|---|---|---|
アプリ本体(SSR と管理画面) | Cloudflare Workers | なし |
コンテンツの DB | D1 |
|
画像などのメディア | R2 |
|
セッション | Workers KV |
|
画像の変換 | Cloudflare Images |
|
予約投稿などの定期処理 | Cron Triggers |
|
使用したバージョン
- Node.js v26.11.1(要件は 22.16 以上)
- pnpm 11.9
- Wrangler 4.149.0
- EmDash 1.2.0
- Astro 7.3.8
1. プロジェクトを作る
create-emdash でプロジェクトを作ります。対話形式でも進められますが、今回はオプションで全部指定しました。
npm create emdash@latest emdash-blog -- --template cloudflare:blog --pm pnpm
主なオプションは次のとおりです。
オプション | 意味 |
|---|---|
|
|
|
|
| Sandboxed Plugins を有効にする。Workers の有料プランが必要(デフォルトはオフ) |
create-emdash は、プラグインのシークレットを暗号化するための EMDASH_ENCRYPTION_KEY を作り、.env に書き込みます。.env は .gitignore に入っていますが、このキーは別にバックアップしておきましょう。キーをなくすと、DB を復元しても、暗号化された設定を読めなくなります。
生成された主なファイル
astro.config.mjs では、D1 と R2 を EmDash に接続しています。
import cloudflare from "@astrojs/cloudflare";
import react from "@astrojs/react";
import { d1, r2 } from "@emdash-cms/cloudflare";
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
export default defineConfig({
output: "server",
adapter: cloudflare(),
integrations: [
react(),
emdash({
database: d1({ binding: "DB", session: "auto" }),
storage: r2({ binding: "MEDIA" }),
}),
],
});src/worker.ts は Worker のエントリーポイントです。Cron から呼ばれる scheduled ハンドラーもここで定義しています。
import handler, { createScheduledHandler, PluginBridge } from "@emdash-cms/cloudflare/worker";
export { PluginBridge };
export default {
...handler,
scheduled: createScheduledHandler(),
} satisfies ExportedHandler;2. リソース名を変える
生成された wrangler.jsonc には、Worker、D1、R2 の名前として my-emdash-site と my-emdash-media が入っています。
以前に EmDash を試したことがあると、同じ名前のリソースがアカウントに残っていることがあります。私のアカウントにも残っていたので、デプロイの前にプロジェクト固有の名前に変えました。
- "name": "my-emdash-site",
+ "name": "emdash-blog",
...
- "database_name": "my-emdash-site",
+ "database_name": "emdash-blog",
...
- "bucket_name": "my-emdash-media",
+ "bucket_name": "emdash-blog-media",アカウントにあるリソースは、次のコマンドで確認できます。
pnpm wrangler d1 list
pnpm wrangler r2 bucket list3. ローカルで動かす
cd emdash-blog
pnpm dev開発サーバーが起動したら、http://localhost:4321 を開きます。止めるときは Ctrl+C を押します。
ローカルでは D1 と R2 を Miniflare がエミュレートするので、Cloudflare に接続しなくてもひととおり試せます。
Claude Code などの AI エージェントからpnpm devを実行すると、Astro 7 は開発サーバーをバックグラウンドで起動し、ログを JSON で出します。その場合はpnpm astro dev stopで止め、pnpm astro dev logsでログを見ます。
初期セットアップウィザード
http://localhost:4321/_emdash/admin を開くと、セットアップウィザードが始まります。
最初の画面で、サイトのタイトルとキャッチフレーズを入力し、サンプルコンテンツを入れるかどうかを選びます。

次の画面で、管理者のメールアドレスと名前を入力します。

最後にパスキーを作ります。EmDash の管理画面にはパスワードがなく、パスキーでログインします。


ウィザードが終わるとログイン画面に移るので、「パスキーでログイン」を押し、いま作ったパスキーでログインします。次回からも、この画面から入ります。メールの送信を設定すると、メールのリンクでもログインできるようになります。

ローカル開発では、http://localhost:4321/_emdash/api/setup/dev-bypass?redirect=/_emdash/admin を開くと、パスキーなしで開発用の管理者としてログインできます。本番環境で開くと 403 が返り、使えないことを確認しました。管理画面
ダッシュボードには、下書き、メディア、ユーザー数、最近の更新が表示されます。管理画面は日本語で表示されますが、一部の項目は英語のままです。

投稿一覧の画面です。

エディターで書いた本文は、Portable Text という形式で保存されます。右側のパネルで、公開状態、スラッグ、執筆者などを設定できます。

公開側の見た目
blog テンプレートには、トップページのヒーロー、記事一覧、カテゴリー、タグ、全文検索、RSS、ダークモードとライトモードの切り替えがそろっています。

記事ページは 3 カラムです。左に執筆者と日付、中央に本文、右に目次と検索が並びます。

4. Cloudflare にデプロイする
Wrangler にログインしていない場合は、先にログインします。
pnpm wrangler loginビルドしてデプロイします。--secrets-file .env を付けると、.env の EMDASH_ENCRYPTION_KEY も Workers の Secret としてアップロードされます。
pnpm astro build
pnpm wrangler deploy --secrets-file .env
wrangler.jsonc に書いた D1 と R2、それに Astro のセッション用の KV は、初回のデプロイで Wrangler が自動で作りました。wrangler d1 create などを前もって実行する必要はありません。
シークレットが平文の変数ではなく Secret として保存されたかは、次のコマンドで確認できます。
pnpm wrangler secret list
# [{ "name": "EMDASH_ENCRYPTION_KEY", "type": "secret_text" }]DB のマイグレーションは、最初のリクエストのときに自動で実行されます。
5. 本番の初期セットアップ
デプロイが終わったら、すぐに https://emdash-blog.<your-subdomain>.workers.dev/_emdash/admin を開いて、セットアップウィザードを済ませます。急いだほうがよい理由は 2 つあります。
- セットアップが終わるまでは、URL を知っている人なら誰でも最初の管理者になれてしまいます。
- パスキーはドメインに結びついています。
localhostで作ったパスキーは本番では使えないので、本番のドメインで登録し直す必要があります。
カスタムドメインで運用する予定なら、先にドメインを設定してから、そのドメインでセットアップするのがおすすめです。workers.dev で登録したパスキーは、カスタムドメインでは使えないためです。あわせて、環境変数 EMDASH_SITE_URL も設定しておきます。
パスキーは 1 ユーザーにつき 10 個まで登録できます。端末をなくしたときのために、予備のパスキーも登録しておくと安心です。
ハマりどころとメモ
- Sandboxed Plugins は Worker Loader を使うので、Workers の有料プランが必要です。無料プランのままなら、
wrangler.jsoncのworker_loadersはコメントアウトしたままにしておきます。 - デフォルトのリソース名
my-emdash-siteが、以前に作ったリソースと重なることがあります(2 章を参照)。 - blog テンプレートでは、投稿へのコメントが最初から有効になっています。公開前にモデレーションを設定するか、使わないなら無効にしておきましょう。
- 予約投稿などの定期処理のために、毎分(
* * * * *)動く Cron Trigger が設定されています。設定はpnpm emdash doctorで確認できます。 - Images バインディングが自動で追加されます。Cloudflare Images の画像変換は課金の対象で、料金は「元の画像と変換パラメーターの組み合わせ」の月ごとの数で決まります。使用量に注意します。
- バックアップでは、D1 のデータ、R2 のメディア、
EMDASH_ENCRYPTION_KEYの 3 つをまとめて保管します。
おまけ: 個人情報を写さずにスクリーンショットを撮る
ブログに載せる画像に、メールアドレスやアカウントの情報が写らないようにしたかったので、次の方法で撮りました。
ブラウザの画面: Playwright と WebAuthn の仮想認証器
ローカル環境のヘッドレス Chrome を、Playwright から操作しました。入力するのは admin@example.com などのダミーデータだけです。パスキーの登録には、Chrome DevTools Protocol の WebAuthn 仮想認証器を使いました。こうすると、実際の端末の生体認証や、パスワードマネージャーの中身が画面に出ません。
import { chromium } from "playwright";
const browser = await chromium.launch({ channel: "chrome" });
const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 }, deviceScaleFactor: 2, locale: "ja-JP" });
const page = await ctx.newPage();
// WebAuthn 仮想認証器(実際の端末や生体情報を使わない)
const cdp = await ctx.newCDPSession(page);
await cdp.send("WebAuthn.enable");
await cdp.send("WebAuthn.addVirtualAuthenticator", {
options: {
protocol: "ctap2",
transport: "internal",
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
automaticPresenceSimulation: true,
},
});
await page.goto("http://localhost:4321/_emdash/admin/setup");
await page.getByLabel("サイトタイトル").fill("EmDash Lab");
// ...ダミーデータで入力しながら page.screenshot() で撮影仮想認証器で作ったパスキーは、ブラウザを閉じると消えます。本番のセットアップでは使わないでください。使うと、二度とログインできなくなります。
ターミナルの画面: 実際のログを置き換えて画像にする
ターミナルの画像には、実際のコマンド出力を使いました。ただし、個人を特定できる値は sed でダミーに置き換えています。
置き換えた値 | 置き換え後 |
|---|---|
メールアドレス |
|
Cloudflare のアカウント ID |
|
workers.dev のサブドメイン |
|
KV の ID とバージョン ID |
|
ホームディレクトリのパス |
|
置き換えたあと、元の値が残っていないことを grep で確かめてから、ターミナル風の HTML の枠に流し込み、Playwright で画像にしました。
おわりに
npm create emdash@latest から wrangler deploy まで、2〜3 本のコマンドで、CMS 付きのブログを Workers 上で公開できました。次は、カスタムドメインの設定と、src/styles/theme.css でのテーマのカスタマイズを試す予定です。