Skip to content

EmDash

Last updated View as MarkdownAgent setup

EmDash ↗︎ is an open-source, full-stack content management system (CMS) for Astro. An EmDash site runs on Workers, stores content in D1, and stores media in R2. It includes an admin panel, a plugin system, and a Model Context Protocol (MCP) server for editing content from AI tools.

The source code is available on GitHub ↗︎.

You need Node.js 22.23.4 or later and a Cloudflare account.

Create an EmDash site

  1. Create a project with create-emdash ↗︎:

    npm create emdash@latest -- my-emdash-site
  2. When prompted, choose the following options:

    • Where will you deploy? Cloudflare Workers
    • Which template? Starter, Blog, Marketing, or Portfolio
    • Install dependencies? Yes
    • Enable sandboxed plugins? No. You can turn them on later. For more information, refer to Sandboxed plugins.

    The scaffolder writes a generated EMDASH_ENCRYPTION_KEY to a .env file. Do not commit this file.

  3. Go to the project directory:

    cd my-emdash-site

To create a project without prompts, pass the template and platform as flags:

npm create emdash@latest -- my-emdash-site --template cloudflare:starter --yes

Cloudflare products EmDash uses

An EmDash site uses the following Cloudflare products:

Product Binding Purpose Configured in
D1 DB Stores content, users, and settings wrangler.jsonc
R2 MEDIA Stores uploaded media wrangler.jsonc
Cron Triggers — Runs scheduled publishing, plugin tasks, and backups wrangler.jsonc
Workers KV SESSION Stores Astro sessions Added by @astrojs/cloudflare at build
Images IMAGES Resizes and optimizes media Added by @astrojs/cloudflare at build
Workers Static Assets ASSETS Serves the built front end and admin panel files Added by @astrojs/cloudflare at build
Dynamic Workers LOADER Runs sandboxed plugins in isolation (optional) wrangler.jsonc

The template configures the D1, R2, and Cron Trigger bindings in wrangler.jsonc:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "my-emdash-site",
  "main": "./src/worker.ts",
  // Set this to today's date
  "compatibility_date": "2026-11-06",
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "my-emdash-site"
    }
  ],
  "r2_buckets": [
    {
      "binding": "MEDIA",
      "bucket_name": "my-emdash-media"
    }
  ],
  "triggers": {
    "crons": [
      "* * * * *"
    ]
  }
}
name = "my-emdash-site"
main = "./src/worker.ts"
# Set this to today's date
compatibility_date = "2026-11-06"

[[d1_databases]]
binding = "DB"
database_name = "my-emdash-site"

[[r2_buckets]]
binding = "MEDIA"
bucket_name = "my-emdash-media"

[triggers]
crons = ["* * * * *"]

The template pins an earlier compatibility date and sets the nodejs_compat flag. Keep the flag unless you update the compatibility date to 2026-08-04 or later, which turns on Node.js compatibility by default.

EmDash connects to these bindings by name in astro.config.mjs. The binding names in both files must match:

astro.config.mjsjs
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", coalesce: true }),
			storage: r2({ binding: "MEDIA" }),
		}),
	],
});

The Worker entry point in src/worker.ts exports a scheduled handler. The Cron Trigger calls this handler to publish scheduled content. Keep both the Cron Trigger and the handler, or scheduled entries do not go live.

Preview and deploy

  1. Build the site:

    npm run build
  2. Preview the site:

    npx wrangler dev

    Open the local URL that Wrangler prints. To start the setup wizard, go to /_emdash/admin/.

  3. Deploy the site:

    npm run deploy

    On the first deploy, Wrangler creates the D1 database, R2 bucket, and KV namespace that the site uses.

  4. Open the workers.dev URL that Wrangler prints, then go to /_emdash/admin/ to complete setup.

To use encrypted plugin settings in production, upload the encryption key from .env as a secret:

npx wrangler secret put EMDASH_ENCRYPTION_KEY

Sandboxed plugins

EmDash can run plugins in isolated sandboxes with Dynamic Workers. Sandboxed plugins cannot access environment variables or the file system.

To turn on sandboxed plugins:

  1. In wrangler.jsonc, uncomment the worker_loaders binding:

    {
      "$schema": "./node_modules/wrangler/config-schema.json",
      "worker_loaders": [
        {
          "binding": "LOADER"
        }
      ]
    }
    [[worker_loaders]]
    binding = "LOADER"
  2. In astro.config.mjs, import sandbox and pass it to EmDash as sandboxRunner:

    astro.config.mjsjs
    import { d1, r2, sandbox } from "@emdash-cms/cloudflare";
    
    emdash({
    	database: d1({ binding: "DB", session: "auto", coalesce: true }),
    	storage: r2({ binding: "MEDIA" }),
    	sandboxRunner: sandbox(),
    });
  3. Build and deploy the site again.

Build with AI tools

Each EmDash template for Cloudflare includes an AGENTS.md file, agent skills, and an MCP configuration for the EmDash documentation ↗︎. AI coding tools that read these files can work with EmDash without extra setup.

Each deployed site also includes an MCP server ↗︎. Connect an MCP client to draft, edit, and schedule content.

Next steps

Was this helpful?