--- name: emdash-cms description: AI coding agent skill for building with EmDash, the full-stack TypeScript CMS built on Astro and Cloudflare triggers: - set up EmDash CMS - add EmDash to my Astro site - create a content type in EmDash - build an EmDash plugin - query EmDash content collections - migrate from WordPress to EmDash - configure EmDash with Cloudflare D1 - add authentication to my EmDash site --- # EmDash CMS > Skill by [ara.so](https://ara.so) — Daily 2026 Skills collection. EmDash is a full-stack TypeScript CMS built on Astro and Cloudflare. It is the spiritual successor to WordPress: extensible, developer-friendly, and powered by a plugin system that runs plugins in sandboxed Worker isolates rather than with full filesystem/database access. EmDash stores rich text as Portable Text (structured JSON) rather than HTML, supports passkey-first auth, and runs on Cloudflare (D1 + R2 + Workers) or any Node.js server with SQLite. --- ## Installation ### Scaffold a new project ```bash npm create emdash@latest ``` Follow the prompts to choose a template (blog, marketing, portfolio, starter, blank) and a platform (Cloudflare or Node.js/SQLite). ### Deploy to Cloudflare directly Use the one-click deploy button from the README, or: ```bash npm create emdash@latest -- --template blog-cloudflare cd my-site npm run deploy ``` ### Add EmDash to an existing Astro project ```bash npm install emdash ``` ```typescript // astro.config.mjs import { defineConfig } from "astro/config"; import emdash from "emdash/astro"; import { d1 } from "emdash/db"; export default defineConfig({ integrations: [ emdash({ database: d1(), // Cloudflare D1 }), ], }); ``` For Node.js + SQLite (no Cloudflare account needed): ```typescript // astro.config.mjs import { defineConfig } from "astro/config"; import emdash from "emdash/astro"; import { sqlite } from "emdash/db"; export default defineConfig({ integrations: [ emdash({ database: sqlite({ path: "./content.db" }), }), ], }); ``` --- ## Key CLI Commands ```bash # Scaffold a new EmDash project npm create emdash@latest # Generate TypeScript types from your live schema npx emdash types # Seed the demo site with sample content npx emdash seed # Run database migrations npx emdash migrate # Start the dev server (standard Astro command) npx astro dev # Build for production npx astro build # Open admin panel (after dev server starts) open http://localhost:4321/_emdash/admin ``` ### Monorepo / contributor commands ```bash pnpm install pnpm build pnpm test # run all tests pnpm typecheck # TypeScript check pnpm lint:quick # fast lint (< 1s) pnpm format # format with oxfmt # Run the demo (Node.js + SQLite, no Cloudflare needed) pnpm --filter emdash-demo seed pnpm --filter emdash-demo dev ``` --- ## Configuration ### Cloudflare (D1 + R2 + KV + Worker Loaders) ```jsonc // wrangler.jsonc { "name": "my-emdash-site", "compatibility_date": "2025-01-01", "d1_databases": [ { "binding": "DB", "database_name": "emdash-content", "database_id": "$DATABASE_ID" } ], "r2_buckets": [ { "binding": "MEDIA", "bucket_name": "emdash-media" } ], "kv_namespaces": [ { "binding": "SESSIONS", "id": "$KV_NAMESPACE_ID" } ], // Remove this block to disable sandboxed plugins (free accounts) "worker_loaders": [ { "binding": "PLUGIN_LOADER" } ] } ``` ```typescript // astro.config.mjs import emdash from "emdash/astro"; import { d1 } from "emdash/db"; import { r2 } from "emdash/storage"; import { kv } from "emdash/sessions"; export default defineConfig({ integrations: [ emdash({ database: d1({ binding: "DB" }), storage: r2({ binding: "MEDIA" }), sessions: kv({ binding: "SESSIONS" }), }), ], }); ``` ### Node.js + SQLite ```typescript // astro.config.mjs import emdash from "emdash/astro"; import { sqlite } from "emdash/db"; import { localFiles } from "emdash/storage"; export default defineConfig({ integrations: [ emdash({ database: sqlite({ path: "./content.db" }), storage: localFiles({ dir: "./public/uploads" }), }), ], }); ``` ### PostgreSQL / Turso ```typescript import { postgres } from "emdash/db"; import { turso } from "emdash/db"; // PostgreSQL database: postgres({ url: process.env.DATABASE_URL }) // Turso/libSQL database: turso({ url: process.env.TURSO_DATABASE_URL, authToken: process.env.TURSO_AUTH_TOKEN, }) ``` --- ## Querying Content Content types are defined in the admin UI (no code required). After creating a collection, generate types: ```bash npx emdash types ``` This writes type definitions to `src/emdash.d.ts`. ### Fetch a collection in an Astro page ```astro --- // src/pages/blog/index.astro import { getEmDashCollection } from "emdash"; const { entries: posts } = await getEmDashCollection("posts", { filter: { status: "published" }, sort: { field: "publishedAt", direction: "desc" }, limit: 10, }); ---
Welcome, {user.name}!
``` ### Get the current user (optional) ```astro --- import { getUser } from "emdash/auth"; const user = await getUser(Astro); // null if not logged in --- {user ?Hello {user.name}
: Sign in} ``` ### Roles ```typescript import { requireRole } from "emdash/auth"; // In an API route or page const user = await requireRole(Astro, "editor"); // Roles: "administrator" | "editor" | "author" | "contributor" ``` --- ## WordPress Migration ### Import from a WXR export file ```bash npx emdash import:wordpress ./export.xml ``` ### Import from the WordPress REST API ```bash npx emdash import:wordpress --url https://yoursite.wordpress.com --api-key $WP_API_KEY ``` ### Import from WordPress.com ```bash npx emdash import:wordpress --wpcom --site yoursite.wordpress.com ``` The importer migrates posts, pages, media attachments, categories, tags, authors, and comments. Gutenberg blocks are converted to Portable Text via the `gutenberg-to-portable-text` package. --- ## Content Schema (Admin UI) Content types are created in the admin panel at `/_emdash/admin` — not in code. After creating or modifying a collection, regenerate TypeScript types: ```bash npx emdash types # Writes to src/emdash.d.ts ``` ### Field types available in the schema builder - `text` — short string - `richText` — Portable Text (TipTap editor) - `number` — integer or float - `boolean` — true/false toggle - `date` / `datetime` — date pickers - `select` — single-choice dropdown - `multiSelect` — multi-choice - `image` — media library picker - `file` — file attachment - `relation` — link to another collection entry - `slug` — URL-safe string, auto-generated from a source field - `json` — raw JSON --- ## MCP Server (AI Tool Integration) EmDash includes a built-in MCP server so AI tools like Claude and ChatGPT can manage site content directly. ```typescript // astro.config.mjs import emdash from "emdash/astro"; export default defineConfig({ integrations: [ emdash({ database: d1(), mcp: { enabled: true, // Restrict to specific roles allowedRoles: ["administrator", "editor"], }, }), ], }); ``` The MCP server is available at `/_emdash/mcp`. Connect Claude Desktop by adding to `claude_desktop_config.json`: ```json { "mcpServers": { "emdash": { "url": "https://yoursite.com/_emdash/mcp", "headers": { "Authorization": "Bearer $EMDASH_MCP_TOKEN" } } } } ``` --- ## Repository Structure ``` packages/ core/ Astro integration, APIs, admin UI, CLI auth/ Authentication library blocks/ Portable Text block definitions cloudflare/ Cloudflare adapter (D1, R2, Worker Loader) plugins/ First-party plugins (forms, embeds, SEO, audit-log, etc.) create-emdash/ npm create emdash scaffolding gutenberg-to-portable-text/ WordPress block converter templates/ blog, marketing, portfolio, starter, blank demos/ Development example sites docs/ Starlight documentation site ``` --- ## First-Party Plugins Install from the `emdash/plugins` package: ```typescript import forms from "emdash/plugins/forms"; import seo from "emdash/plugins/seo"; import embeds from "emdash/plugins/embeds"; import auditLog from "emdash/plugins/audit-log"; export default defineConfig({ integrations: [ emdash({ database: d1(), plugins: [forms(), seo(), embeds(), auditLog()], }), ], }); ``` --- ## Troubleshooting ### "Dynamic Workers are not available on free accounts" Sandboxed plugins require a paid Cloudflare account ($5/mo+). To disable sandboxed plugins and run them in-process instead, remove the `worker_loaders` block from `wrangler.jsonc`: ```jsonc // wrangler.jsonc — remove this block on free accounts // "worker_loaders": [{ "binding": "PLUGIN_LOADER" }] ``` ### Admin panel returns 404 Ensure the Astro integration is registered in `astro.config.mjs` and the dev server has been restarted after installing EmDash. ### TypeScript errors after schema changes Regenerate types after modifying collections in the admin UI: ```bash npx emdash types ``` ### D1 binding errors locally Use `wrangler dev` instead of `astro dev` when developing with D1 bindings, or switch to SQLite for local development: ```typescript database: process.env.CF_PAGES ? d1() : sqlite({ path: "./content.db" }) ``` ### Migrations not applied ```bash npx emdash migrate # For Cloudflare D1 remote: npx wrangler d1 migrations apply emdash-content --remote ``` ### Plugin hook not firing Verify the plugin is listed in the `plugins` array in `astro.config.mjs` and that the capability required by the hook is declared in the plugin's `capabilities` array. Missing capabilities cause the sandbox to silently block the call.