emdash-cms
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseEmDash CMS
EmDash CMS
Skill by 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.
技能由 ara.so 开发 — 2026年度每日技能合集。
EmDash是基于Astro和Cloudflare构建的全栈TypeScript CMS,它是WordPress的精神继任者:可扩展、对开发者友好,其插件系统在沙箱化的Worker隔离环境中运行插件,而非赋予插件完整的文件系统/数据库访问权限。EmDash将富文本存储为Portable Text(结构化JSON)而非HTML,支持通行密钥优先的认证体系,可运行在Cloudflare(D1 + R2 + Workers)或者任何搭载SQLite的Node.js服务器上。
Installation
安装
Scaffold a new project
初始化新项目
bash
npm create emdash@latestFollow the prompts to choose a template (blog, marketing, portfolio, starter, blank) and a platform (Cloudflare or Node.js/SQLite).
bash
npm create emdash@latest按照提示选择模板(博客、营销站点、作品集、启动模板、空白模板)和平台(Cloudflare或Node.js/SQLite)。
Deploy to Cloudflare directly
直接部署到Cloudflare
Use the one-click deploy button from the README, or:
bash
npm create emdash@latest -- --template blog-cloudflare
cd my-site
npm run deploy使用README中的一键部署按钮,或者执行以下命令:
bash
npm create emdash@latest -- --template blog-cloudflare
cd my-site
npm run deployAdd EmDash to an existing Astro project
将EmDash添加到现有Astro项目
bash
npm install emdashtypescript
// 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" }),
}),
],
});bash
npm install emdashtypescript
// 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
}),
],
});如果使用Node.js + SQLite(无需Cloudflare账号):
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
核心CLI命令
bash
undefinedbash
undefinedScaffold a new EmDash project
初始化新的EmDash项目
npm create emdash@latest
npm create emdash@latest
Generate TypeScript types from your live schema
根据你线上的 schema 生成TypeScript类型定义
npx emdash types
npx emdash types
Seed the demo site with sample content
用示例内容初始化演示站点
npx emdash seed
npx emdash seed
Run database migrations
执行数据库迁移
npx emdash migrate
npx emdash migrate
Start the dev server (standard Astro command)
启动开发服务器(标准Astro命令)
npx astro dev
npx astro dev
Build for production
构建生产版本
npx astro build
npx astro build
Open admin panel (after dev server starts)
打开管理面板(开发服务器启动后执行)
undefinedundefinedMonorepo / contributor commands
Monorepo/贡献者命令
bash
pnpm install
pnpm build
pnpm test # run all tests
pnpm typecheck # TypeScript check
pnpm lint:quick # fast lint (< 1s)
pnpm format # format with oxfmtbash
pnpm install
pnpm build
pnpm test # run all tests
pnpm typecheck # TypeScript check
pnpm lint:quick # fast lint (< 1s)
pnpm format # format with oxfmtRun the demo (Node.js + SQLite, no Cloudflare needed)
Run the demo (Node.js + SQLite, no Cloudflare needed)
pnpm --filter emdash-demo seed
pnpm --filter emdash-demo dev
---pnpm --filter emdash-demo seed
pnpm --filter emdash-demo dev
---Configuration
配置
Cloudflare (D1 + R2 + KV + Worker Loaders)
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" }),
}),
],
});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
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" }),
}),
],
});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
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,
})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 typesThis writes type definitions to .
src/emdash.d.ts内容类型在管理UI中定义(无需编写代码),创建集合后,执行以下命令生成类型定义:
bash
npx emdash types该命令会将类型定义写入文件。
src/emdash.d.tsFetch a collection in an Astro page
在Astro页面中获取集合内容
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,
});
---
<ul>
{posts.map((post) => (
<li>
<a href={`/blog/${post.data.slug}`}>{post.data.title}</a>
<time datetime={post.data.publishedAt}>{post.data.publishedAt}</time>
</li>
))}
</ul>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,
});
---
<ul>
{posts.map((post) => (
<li>
<a href={`/blog/${post.data.slug}`}>{post.data.title}</a>
<time datetime={post.data.publishedAt}>{post.data.publishedAt}</time>
</li>
))}
</ul>Fetch a single entry
获取单条内容
astro
---
// src/pages/blog/[slug].astro
import { getEmDashEntry, renderPortableText } from "emdash";
const { slug } = Astro.params;
const post = await getEmDashEntry("posts", { slug });
if (!post) return Astro.redirect("/404");
const { Content } = await renderPortableText(post.data.body);
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>astro
---
// src/pages/blog/[slug].astro
import { getEmDashEntry, renderPortableText } from "emdash";
const { slug } = Astro.params;
const post = await getEmDashEntry("posts", { slug });
if (!post) return Astro.redirect("/404");
const { Content } = await renderPortableText(post.data.body);
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>Pagination
分页
astro
---
import { getEmDashCollection } from "emdash";
const page = Number(Astro.params.page ?? 1);
const { entries: posts, total } = await getEmDashCollection("posts", {
filter: { status: "published" },
sort: { field: "publishedAt", direction: "desc" },
limit: 10,
offset: (page - 1) * 10,
});
---astro
---
import { getEmDashCollection } from "emdash";
const page = Number(Astro.params.page ?? 1);
const { entries: posts, total } = await getEmDashCollection("posts", {
filter: { status: "published" },
sort: { field: "publishedAt", direction: "desc" },
limit: 10,
offset: (page - 1) * 10,
});
---Filtering by taxonomy
按分类筛选
astro
---
import { getEmDashCollection } from "emdash";
const { entries: posts } = await getEmDashCollection("posts", {
filter: {
status: "published",
tags: { contains: "typescript" },
},
});
---astro
---
import { getEmDashCollection } from "emdash";
const { entries: posts } = await getEmDashCollection("posts", {
filter: {
status: "published",
tags: { contains: "typescript" },
},
});
---Portable Text Rendering
Portable Text渲染
EmDash stores rich text as Portable Text (structured JSON), not HTML.
astro
---
import { renderPortableText } from "emdash";
const post = await getEmDashEntry("posts", { slug: Astro.params.slug });
const { Content } = await renderPortableText(post.data.body);
---
<Content />EmDash将富文本存储为Portable Text(结构化JSON),而非HTML。
astro
---
import { renderPortableText } from "emdash";
const post = await getEmDashEntry("posts", { slug: Astro.params.slug });
const { Content } = await renderPortableText(post.data.body);
---
<Content />Custom block renderers
自定义块渲染器
typescript
// src/portable-text.ts
import { definePortableTextComponents } from "emdash/blocks";
export const components = definePortableTextComponents({
types: {
callout: ({ value }) => `
<div class="callout callout--${value.type}">
${value.text}
</div>
`,
image: ({ value }) => `
<figure>
<img src="${value.url}" alt="${value.alt ?? ""}" />
${value.caption ? `<figcaption>${value.caption}</figcaption>` : ""}
</figure>
`,
},
marks: {
highlight: ({ children }) => `<mark>${children}</mark>`,
},
});astro
---
import { renderPortableText } from "emdash";
import { components } from "../portable-text";
const { Content } = await renderPortableText(post.data.body, { components });
---
<Content />typescript
// src/portable-text.ts
import { definePortableTextComponents } from "emdash/blocks";
export const components = definePortableTextComponents({
types: {
callout: ({ value }) => `
<div class="callout callout--${value.type}">
${value.text}
</div>
`,
image: ({ value }) => `
<figure>
<img src="${value.url}" alt="${value.alt ?? ""}" />
${value.caption ? `<figcaption>${value.caption}</figcaption>` : ""}
</figure>
`,
},
marks: {
highlight: ({ children }) => `<mark>${children}</mark>`,
},
});astro
---
import { renderPortableText } from "emdash";
import { components } from "../portable-text";
const { Content } = await renderPortableText(post.data.body, { components });
---
<Content />Plugin Development
插件开发
Plugins are the primary extension mechanism. On Cloudflare, they run in sandboxed Worker isolates with a declared capability manifest. On Node.js, they run in-process (safe mode).
插件是主要的扩展机制。在Cloudflare环境中,插件在沙箱化的Worker隔离环境中运行,附带声明的权限清单;在Node.js环境中,插件在进程内运行(安全模式)。
Minimal plugin
最小插件示例
typescript
// plugins/my-plugin/index.ts
import { definePlugin } from "emdash/plugin";
export default () =>
definePlugin({
id: "my-plugin",
name: "My Plugin",
version: "1.0.0",
capabilities: [],
hooks: {},
});typescript
// plugins/my-plugin/index.ts
import { definePlugin } from "emdash/plugin";
export default () =>
definePlugin({
id: "my-plugin",
name: "My Plugin",
version: "1.0.0",
capabilities: [],
hooks: {},
});Plugin with content hooks and email
带内容钩子和邮件功能的插件
typescript
import { definePlugin } from "emdash/plugin";
export default () =>
definePlugin({
id: "notify-on-publish",
name: "Notify on Publish",
version: "1.0.0",
capabilities: ["read:content", "email:send"],
hooks: {
"content:afterSave": async (event, ctx) => {
if (event.content.status !== "published") return;
await ctx.email.send({
to: "editors@example.com",
subject: `New post published: ${event.content.title}`,
text: `"${event.content.title}" is now live.`,
});
},
},
});typescript
import { definePlugin } from "emdash/plugin";
export default () =>
definePlugin({
id: "notify-on-publish",
name: "Notify on Publish",
version: "1.0.0",
capabilities: ["read:content", "email:send"],
hooks: {
"content:afterSave": async (event, ctx) => {
if (event.content.status !== "published") return;
await ctx.email.send({
to: "editors@example.com",
subject: `New post published: ${event.content.title}`,
text: `"${event.content.title}" is now live.`,
});
},
},
});Plugin with KV storage and admin settings
带KV存储和管理设置的插件
typescript
import { definePlugin } from "emdash/plugin";
export default () =>
definePlugin({
id: "view-counter",
name: "View Counter",
version: "1.0.0",
capabilities: ["read:content", "kv:read", "kv:write"],
settings: {
schema: {
resetDaily: { type: "boolean", default: false, label: "Reset counts daily" },
},
},
hooks: {
"content:beforeRender": async (event, ctx) => {
const key = `views:${event.content.id}`;
const current = Number(await ctx.kv.get(key) ?? 0);
await ctx.kv.set(key, String(current + 1));
event.content.meta.views = current + 1;
},
},
adminPages: [
{
path: "/analytics",
title: "View Analytics",
component: "ViewAnalytics",
},
],
});typescript
import { definePlugin } from "emdash/plugin";
export default () =>
definePlugin({
id: "view-counter",
name: "View Counter",
version: "1.0.0",
capabilities: ["read:content", "kv:read", "kv:write"],
settings: {
schema: {
resetDaily: { type: "boolean", default: false, label: "Reset counts daily" },
},
},
hooks: {
"content:beforeRender": async (event, ctx) => {
const key = `views:${event.content.id}`;
const current = Number(await ctx.kv.get(key) ?? 0);
await ctx.kv.set(key, String(current + 1));
event.content.meta.views = current + 1;
},
},
adminPages: [
{
path: "/analytics",
title: "View Analytics",
component: "ViewAnalytics",
},
],
});Plugin with custom block type
带自定义块类型的插件
typescript
import { definePlugin } from "emdash/plugin";
import { defineBlock } from "emdash/blocks";
export default () =>
definePlugin({
id: "callout-block",
name: "Callout Block",
version: "1.0.0",
capabilities: [],
blocks: [
defineBlock({
name: "callout",
title: "Callout",
fields: [
{ name: "type", type: "select", options: ["info", "warning", "danger"], default: "info" },
{ name: "text", type: "text", label: "Message" },
],
}),
],
hooks: {},
});typescript
import { definePlugin } from "emdash/plugin";
import { defineBlock } from "emdash/blocks";
export default () =>
definePlugin({
id: "callout-block",
name: "Callout Block",
version: "1.0.0",
capabilities: [],
blocks: [
defineBlock({
name: "callout",
title: "Callout",
fields: [
{ name: "type", type: "select", options: ["info", "warning", "danger"], default: "info" },
{ name: "text", type: "text", label: "Message" },
],
}),
],
hooks: {},
});Plugin with custom API route
带自定义API路由的插件
typescript
import { definePlugin } from "emdash/plugin";
export default () =>
definePlugin({
id: "newsletter",
name: "Newsletter",
version: "1.0.0",
capabilities: ["kv:write"],
apiRoutes: [
{
method: "POST",
path: "/subscribe",
handler: async (request, ctx) => {
const { email } = await request.json();
await ctx.kv.set(`subscriber:${email}`, "true");
return new Response(JSON.stringify({ ok: true }), {
headers: { "Content-Type": "application/json" },
});
},
},
],
hooks: {},
});typescript
import { definePlugin } from "emdash/plugin";
export default () =>
definePlugin({
id: "newsletter",
name: "Newsletter",
version: "1.0.0",
capabilities: ["kv:write"],
apiRoutes: [
{
method: "POST",
path: "/subscribe",
handler: async (request, ctx) => {
const { email } = await request.json();
await ctx.kv.set(`subscriber:${email}`, "true");
return new Response(JSON.stringify({ ok: true }), {
headers: { "Content-Type": "application/json" },
});
},
},
],
hooks: {},
});Available capabilities
可用权限列表
| Capability | What it allows |
|---|---|
| Read published content |
| Create and update content |
| Read user profiles |
| Send email via configured provider |
| Read from plugin's KV namespace |
| Write to plugin's KV namespace |
| Make outbound HTTP requests |
| Read from media storage |
| Write to media storage |
| 权限 | 允许操作 |
|---|---|
| 读取已发布内容 |
| 创建和更新内容 |
| 读取用户资料 |
| 通过配置的服务商发送邮件 |
| 读取插件所属KV命名空间的数据 |
| 向插件所属KV命名空间写入数据 |
| 发起出站HTTP请求 |
| 读取媒体存储中的内容 |
| 向媒体存储写入内容 |
Available hooks
可用钩子
typescript
// Content lifecycle
"content:beforeSave"
"content:afterSave"
"content:beforeDelete"
"content:afterDelete"
"content:beforeRender"
// Auth lifecycle
"auth:afterLogin"
"auth:afterLogout"
"auth:afterRegister"
// Media lifecycle
"media:beforeUpload"
"media:afterUpload"
"media:beforeDelete"
// Admin lifecycle
"admin:init"typescript
// Content lifecycle
"content:beforeSave"
"content:afterSave"
"content:beforeDelete"
"content:afterDelete"
"content:beforeRender"
// Auth lifecycle
"auth:afterLogin"
"auth:afterLogout"
"auth:afterRegister"
// Media lifecycle
"media:beforeUpload"
"media:afterUpload"
"media:beforeDelete"
// Admin lifecycle
"admin:init"Authentication
认证
EmDash uses passkey-first (WebAuthn) authentication with OAuth and magic link fallbacks.
EmDash采用通行密钥优先(WebAuthn)的认证体系,同时提供OAuth和魔法链接作为备选方案。
Configure OAuth providers
配置OAuth服务商
typescript
// astro.config.mjs
import emdash from "emdash/astro";
import { github, google } from "emdash/auth";
export default defineConfig({
integrations: [
emdash({
database: d1(),
auth: {
providers: [
github({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
}),
google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
},
}),
],
});typescript
// astro.config.mjs
import emdash from "emdash/astro";
import { github, google } from "emdash/auth";
export default defineConfig({
integrations: [
emdash({
database: d1(),
auth: {
providers: [
github({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
}),
google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
},
}),
],
});Configure magic link email
配置魔法链接邮件
typescript
auth: {
providers: [
magicLink({
from: "noreply@yourdomain.com",
// uses the configured email adapter
}),
],
}typescript
auth: {
providers: [
magicLink({
from: "noreply@yourdomain.com",
// uses the configured email adapter
}),
],
}Protect a page
保护页面访问
astro
---
// src/pages/dashboard.astro
import { requireAuth } from "emdash/auth";
const user = await requireAuth(Astro);
// Redirects to /login if not authenticated
---
<p>Welcome, {user.name}!</p>astro
---
// src/pages/dashboard.astro
import { requireAuth } from "emdash/auth";
const user = await requireAuth(Astro);
// Redirects to /login if not authenticated
---
<p>Welcome, {user.name}!</p>Get the current user (optional)
获取当前登录用户(可选)
astro
---
import { getUser } from "emdash/auth";
const user = await getUser(Astro); // null if not logged in
---
{user ? <p>Hello {user.name}</p> : <a href="/login">Sign in</a>}astro
---
import { getUser } from "emdash/auth";
const user = await getUser(Astro); // null if not logged in
---
{user ? <p>Hello {user.name}</p> : <a href="/login">Sign in</a>}Roles
角色
typescript
import { requireRole } from "emdash/auth";
// In an API route or page
const user = await requireRole(Astro, "editor");
// Roles: "administrator" | "editor" | "author" | "contributor"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
WordPress迁移
Import from a WXR export file
从WXR导出文件导入
bash
npx emdash import:wordpress ./export.xmlbash
npx emdash import:wordpress ./export.xmlImport from the WordPress REST API
从WordPress REST API导入
bash
npx emdash import:wordpress --url https://yoursite.wordpress.com --api-key $WP_API_KEYbash
npx emdash import:wordpress --url https://yoursite.wordpress.com --api-key $WP_API_KEYImport from WordPress.com
从WordPress.com导入
bash
npx emdash import:wordpress --wpcom --site yoursite.wordpress.comThe importer migrates posts, pages, media attachments, categories, tags, authors, and comments. Gutenberg blocks are converted to Portable Text via the package.
gutenberg-to-portable-textbash
npx emdash import:wordpress --wpcom --site yoursite.wordpress.com导入工具会迁移文章、页面、媒体附件、分类、标签、作者和评论。Gutenberg块会通过包转换为Portable Text格式。
gutenberg-to-portable-textContent Schema (Admin UI)
内容Schema(管理UI)
Content types are created in the admin panel at — not in code. After creating or modifying a collection, regenerate TypeScript types:
/_emdash/adminbash
npx emdash types内容类型在管理面板中创建,无需编写代码。创建或修改集合后,重新生成TypeScript类型定义:
/_emdash/adminbash
npx emdash typesWrites to src/emdash.d.ts
Writes to src/emdash.d.ts
undefinedundefinedField types available in the schema builder
Schema构建器中可用的字段类型
- — short string
text - — Portable Text (TipTap editor)
richText - — integer or float
number - — true/false toggle
boolean - /
date— date pickersdatetime - — single-choice dropdown
select - — multi-choice
multiSelect - — media library picker
image - — file attachment
file - — link to another collection entry
relation - — URL-safe string, auto-generated from a source field
slug - — raw JSON
json
- — 短字符串
text - — Portable Text(TipTap编辑器)
richText - — 整数或浮点数
number - — 是/否开关
boolean - /
date— 日期选择器datetime - — 单选下拉框
select - — 多选下拉框
multiSelect - — 媒体库选择器
image - — 文件附件
file - — 关联到其他集合的条目
relation - — URL安全字符串,可从源字段自动生成
slug - — 原始JSON
json
MCP Server (AI Tool Integration)
MCP服务器(AI工具集成)
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 . Connect Claude Desktop by adding to :
/_emdash/mcpclaude_desktop_config.jsonjson
{
"mcpServers": {
"emdash": {
"url": "https://yoursite.com/_emdash/mcp",
"headers": {
"Authorization": "Bearer $EMDASH_MCP_TOKEN"
}
}
}
}EmDash内置了MCP服务器,因此Claude、ChatGPT等AI工具可以直接管理站点内容。
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"],
},
}),
],
});MCP服务器的访问地址为,将以下配置添加到即可连接Claude Desktop:
/_emdash/mcpclaude_desktop_config.jsonjson
{
"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 sitepackages/
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 siteFirst-Party Plugins
官方插件
Install from the package:
emdash/pluginstypescript
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()],
}),
],
});从包安装:
emdash/pluginstypescript
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"
"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 block from :
worker_loaderswrangler.jsoncjsonc
// wrangler.jsonc — remove this block on free accounts
// "worker_loaders": [{ "binding": "PLUGIN_LOADER" }]沙箱化插件需要付费的Cloudflare账号(每月5美元及以上)。如果要禁用沙箱化插件,改为在进程内运行,请移除中的块:
wrangler.jsoncworker_loadersjsonc
// wrangler.jsonc — remove this block on free accounts
// "worker_loaders": [{ "binding": "PLUGIN_LOADER" }]Admin panel returns 404
管理面板返回404
Ensure the Astro integration is registered in and the dev server has been restarted after installing EmDash.
astro.config.mjs请确保中已注册Astro集成,并且安装EmDash后已经重启了开发服务器。
astro.config.mjsTypeScript errors after schema changes
Schema修改后出现TypeScript错误
Regenerate types after modifying collections in the admin UI:
bash
npx emdash types在管理UI中修改集合后重新生成类型定义:
bash
npx emdash typesD1 binding errors locally
本地开发时出现D1绑定错误
Use instead of when developing with D1 bindings, or switch to SQLite for local development:
wrangler devastro devtypescript
database: process.env.CF_PAGES ? d1() : sqlite({ path: "./content.db" })使用D1绑定时请用替代启动服务,或者本地开发时切换为SQLite:
wrangler devastro devtypescript
database: process.env.CF_PAGES ? d1() : sqlite({ path: "./content.db" })Migrations not applied
迁移未生效
bash
npx emdash migratebash
npx emdash migrateFor Cloudflare D1 remote:
For Cloudflare D1 remote:
npx wrangler d1 migrations apply emdash-content --remote
undefinednpx wrangler d1 migrations apply emdash-content --remote
undefinedPlugin hook not firing
插件钩子未触发
Verify the plugin is listed in the array in and that the capability required by the hook is declared in the plugin's array. Missing capabilities cause the sandbox to silently block the call.
pluginsastro.config.mjscapabilities请确认插件已添加到的数组中,并且钩子所需的权限已在插件的数组中声明。缺少权限会导致沙箱静默拦截调用。
astro.config.mjspluginscapabilities