emdash-cms

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

EmDash 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@latest
Follow 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 deploy

Add EmDash to an existing Astro project

将EmDash添加到现有Astro项目

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" }),
    }),
  ],
});

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
    }),
  ],
});
如果使用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
undefined
bash
undefined

Scaffold 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)

打开管理面板(开发服务器启动后执行)

Monorepo / 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 oxfmt
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)

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 types
This writes type definitions to
src/emdash.d.ts
.
内容类型在管理UI中定义(无需编写代码),创建集合后,执行以下命令生成类型定义:
bash
npx emdash types
该命令会将类型定义写入
src/emdash.d.ts
文件。

Fetch 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

可用权限列表

CapabilityWhat it allows
read:content
Read published content
write:content
Create and update content
read:users
Read user profiles
email:send
Send email via configured provider
kv:read
Read from plugin's KV namespace
kv:write
Write to plugin's KV namespace
http:fetch
Make outbound HTTP requests
storage:read
Read from media storage
storage:write
Write to media storage
权限允许操作
read:content
读取已发布内容
write:content
创建和更新内容
read:users
读取用户资料
email:send
通过配置的服务商发送邮件
kv:read
读取插件所属KV命名空间的数据
kv:write
向插件所属KV命名空间写入数据
http:fetch
发起出站HTTP请求
storage:read
读取媒体存储中的内容
storage:write
向媒体存储写入内容

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.xml
bash
npx emdash import:wordpress ./export.xml

Import from the WordPress REST API

从WordPress REST API导入

bash
npx emdash import:wordpress --url https://yoursite.wordpress.com --api-key $WP_API_KEY
bash
npx emdash import:wordpress --url https://yoursite.wordpress.com --api-key $WP_API_KEY

Import from WordPress.com

从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.

bash
npx emdash import:wordpress --wpcom --site yoursite.wordpress.com
导入工具会迁移文章、页面、媒体附件、分类、标签、作者和评论。Gutenberg块会通过
gutenberg-to-portable-text
包转换为Portable Text格式。

Content Schema (Admin UI)

内容Schema(管理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
内容类型在管理面板
/_emdash/admin
中创建,无需编写代码。创建或修改集合后,重新生成TypeScript类型定义:
bash
npx emdash types

Writes to src/emdash.d.ts

Writes to src/emdash.d.ts

undefined
undefined

Field types available in the schema builder

Schema构建器中可用的字段类型

  • 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

  • text
    — 短字符串
  • richText
    — Portable Text(TipTap编辑器)
  • number
    — 整数或浮点数
  • boolean
    — 是/否开关
  • date
    /
    datetime
    — 日期选择器
  • select
    — 单选下拉框
  • multiSelect
    — 多选下拉框
  • image
    — 媒体库选择器
  • file
    — 文件附件
  • relation
    — 关联到其他集合的条目
  • slug
    — URL安全字符串,可从源字段自动生成
  • 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
/_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"
      }
    }
  }
}

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服务器的访问地址为
/_emdash/mcp
,将以下配置添加到
claude_desktop_config.json
即可连接Claude Desktop:
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

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()],
    }),
  ],
});

emdash/plugins
包安装:
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"

"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" }]
沙箱化插件需要付费的Cloudflare账号(每月5美元及以上)。如果要禁用沙箱化插件,改为在进程内运行,请移除
wrangler.jsonc
中的
worker_loaders
块:
jsonc
// 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
astro.config.mjs
and the dev server has been restarted after installing EmDash.
请确保
astro.config.mjs
中已注册Astro集成,并且安装EmDash后已经重启了开发服务器。

TypeScript errors after schema changes

Schema修改后出现TypeScript错误

Regenerate types after modifying collections in the admin UI:
bash
npx emdash types
在管理UI中修改集合后重新生成类型定义:
bash
npx emdash types

D1 binding errors locally

本地开发时出现D1绑定错误

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" })
使用D1绑定时请用
wrangler dev
替代
astro dev
启动服务,或者本地开发时切换为SQLite:
typescript
database: process.env.CF_PAGES ? d1() : sqlite({ path: "./content.db" })

Migrations not applied

迁移未生效

bash
npx emdash migrate
bash
npx emdash migrate

For Cloudflare D1 remote:

For Cloudflare D1 remote:

npx wrangler d1 migrations apply emdash-content --remote
undefined
npx wrangler d1 migrations apply emdash-content --remote
undefined

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.
请确认插件已添加到
astro.config.mjs
plugins
数组中,并且钩子所需的权限已在插件的
capabilities
数组中声明。缺少权限会导致沙箱静默拦截调用。