Communitygithub.com

EmDash CMS

用于构建 EmDash 的 AI 编码代理技能,EmDash 是一个基于 Astro 和 Cloudflare 构建的全栈 TypeScript CMS。

EmDash CMS 是什么?

EmDash CMS 是一款专为 AI 编码代理(如 Claude Code、Cursor、Codex)设计的技能,旨在帮助开发者使用 EmDash 快速构建内容管理系统。EmDash 是一个全栈 TypeScript CMS,基于 Astro 和 Cloudflare 构建,被视为 WordPress 的精神继承者:可扩展、对开发者友好,并采用插件系统,插件在沙箱化的 Worker 隔离环境中运行,而非直接访问文件系统或数据库。该技能通过提供详细的安装指南、配置选项、内容查询 API、Portable Text 渲染、插件开发文档和身份验证设置,使 AI 代理能够高效地生成、修改或管理 EmDash 项目。它支持 Cloudflare (D1 + R2 + Workers) 或任何 Node.js 服务器(使用 SQLite),并内置了 WordPress 迁移工具和 MCP 服务器,简化了内容迁移和 AI 工具集成的流程。通过利用自然语言指令,开发者可以快速搭建博客、营销页面或自定义内容站点,而无需深入了解底层基础设施。

兼容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/reason-machines/trending-skills/tree/HEAD/skills/emdash-cms

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

预览

来自技能 README

EmDash CMS preview

文档

EmDash CMS 是做什么的?

技能由 ara.so 提供 — Daily 2026 技能集。

EmDash 是一款基于 Astro 和 Cloudflare 构建的全栈 TypeScript CMS。它是 WordPress 的精神继承者:可扩展、对开发者友好,并采用插件系统,插件在沙箱化的 Worker 隔离环境中运行,而非直接访问文件系统或数据库。EmDash 将富文本存储为 Portable Text(结构化 JSON)而非 HTML,支持无密码优先的身份验证,可在 Cloudflare(D1 + R2 + Workers)或任何带有 SQLite 的 Node.js 服务器上运行。


安装

搭建新项目

npm create emdash@latest

按照提示选择一个模板(博客、营销、作品集、初学者、空白)和一个平台(Cloudflare 或 Node.js/SQLite)。

直接部署到 Cloudflare

使用 README 中的一键部署按钮,或运行:

npm create emdash@latest -- --template blog-cloudflare
cd my-site
npm run deploy

将 EmDash 添加到现有 Astro 项目

npm install emdash
// 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 账户):

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

关键 CLI 命令

# 搭建新的 EmDash 项目
npm create emdash@latest

# 从实时模式生成 TypeScript 类型
npx emdash types

# 用示例内容填充演示站点
npx emdash seed

# 运行数据库迁移
npx emdash migrate

# 启动开发服务器(标准 Astro 命令)
npx astro dev

# 构建生产版本
npx astro build

# 打开管理面板(在开发服务器启动后)
open http://localhost:4321/_emdash/admin

单体仓库 / 贡献者命令

pnpm install
pnpm build
pnpm test          # 运行所有测试
pnpm typecheck     # TypeScript 检查
pnpm lint:quick    # 快速 lint(< 1s)
pnpm format        # 使用 oxfmt 格式化

# 运行演示(Node.js + SQLite,无需 Cloudflare)
pnpm --filter emdash-demo seed
pnpm --filter emdash-demo dev

配置

Cloudflare (D1 + R2 + KV + Worker Loaders)

// 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"
    }
  ],
  // 移除以下块以禁用沙箱插件(免费账户)
  "worker_loaders": [
    {
      "binding": "PLUGIN_LOADER"
    }
  ]
}
// 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

// 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

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,
})

查询内容

内容类型在管理 UI 中定义(无需编写代码)。创建集合后,生成类型:

npx emdash types

这将类型定义写入 src/emdash.d.ts。

在 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>

获取单个条目

---
// 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>

分页

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

按分类过滤

---
import { getEmDashCollection } from "emdash";

const { entries: posts } = await getEmDashCollection("posts", {
  filter: {
    status: "published",
    tags: { contains: "typescript" },
  },
});
---

Portable Text 渲染

EmDash 将富文本存储为 Portable Text(结构化 JSON),而非 HTML。

---
import { renderPortableText } from "emdash";

const post = await getEmDashEntry("posts", { slug: Astro.params.slug });
const { Content } = await renderPortableText(post.data.body);
---

<Content />

自定义块渲染器

// 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>`,
  },
});
---
import { renderPortableText } from "emdash";
import { components } from "../portable-text";

const { Content } = await renderPortableText(post.data.body, { components });
---
<Content />

插件开发

插件是主要的扩展机制。在 Cloudflare 上,它们在沙箱化的 Worker 隔离环境中运行,并带有声明的能力清单。在 Node.js 上,它们以进程内方式运行(安全模式)。

最小插件

// 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: {},
  });

带内容钩子和邮件功能的插件

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: "[email protected]",
          subject: `New post published: ${event.content.title}`,
          text: `"${event.content.title}" is now live.`,
        });
      },
    },
  });

带 KV 存储和管理设置的插件

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

带自定义块类型的插件

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: {},
  });

带自定义 API 路由的插件

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: {},
  });

可用能力

能力允许的操作
read:content读取已发布的内容
write:content创建和更新内容
read:users读取用户配置文件
email:send通过配置的提供商发送邮件
kv:read从插件的 KV 命名空间读取
kv:write写入插件的 KV 命名空间
http:fetch发起出站 HTTP 请求
storage:read从媒体存储读取
storage:write写入媒体存储

可用钩子

// 内容生命周期
"content:beforeSave"
"content:afterSave"
"content:beforeDelete"
"content:afterDelete"
"content:beforeRender"

// 认证生命周期
"auth:afterLogin"
"auth:afterLogout"
"auth:afterRegister"

// 媒体生命周期
"media:beforeUpload"
"media:afterUpload"
"media:beforeDelete"

// 管理生命周期
"admin:init"

身份验证

EmDash 使用无密码优先(WebAuthn)身份验证,并支持 OAuth 和魔法链接作为备选方案。

配置 OAuth 提供商

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

配置魔法链接邮件

auth: {
  providers: [
    magicLink({
      from: "[email protected]",
      // 使用已配置的邮件适配器
    }),
  ],
}

保护页面

---
// src/pages/dashboard.astro
import { requireAuth } from "emdash/auth";

const user = await requireAuth(Astro);
// 如果未认证,则重定向到 /login
---

<p>Welcome, {user.name}!</p>

获取当前用户(可选)

---
import { getUser } from "emdash/auth";

const user = await getUser(Astro); // 如果未登录,则返回 null
---

{user ? <p>Hello {user.name}</p> : <a href="/login">Sign in</a>}

角色

import { requireRole } from "emdash/auth";

// 在 API 路由或页面中
const user = await requireRole(Astro, "editor");
// 角色:"administrator" | "editor" | "author" | "contributor"

WordPress 迁移

从 WXR 导出文件导入

npx emdash import:wordpress ./export.xml

从 WordPress REST API 导入

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

从 WordPress.com 导入

npx emdash import:wordpress --wpcom --site yoursite.wordpress.com

导入程序会迁移文章、页面、媒体附件、分类、标签、作者和评论。Gutenberg 块通过 gutenberg-to-portable-text 包转换为 Portable Text。


内容模式(管理 UI)

内容类型在管理面板中创建(路径 /_emdash/admin),而非通过代码。创建或修改集合后,重新生成 TypeScript 类型:

npx emdash types
# 写入 src/emdash.d.ts

模式构建器中可用的字段类型

  • text — 短字符串
  • richText — Portable Text(TipTap 编辑器)
  • number — 整数或浮点数
  • boolean — true/false 开关
  • date / datetime — 日期选择器
  • select — 单选下拉菜单
  • multiSelect — 多选
  • image — 媒体库选择器
  • file — 文件附件
  • relation — 链接到另一个集合条目
  • slug — URL 安全字符串,从源字段自动生成
  • json — 原始 JSON

MCP 服务器(AI 工具集成)

EmDash 包含一个内置的 MCP 服务器,用于与 AI 编码代理集成。当前版本支持以下工具:

  • Content Queries — 通过自然语言搜索和获取内容条目。
  • Schema Management — 查看和修改内容类型定义。
  • Plugin Management — 列出、安装和配置插件。
  • Migration Tasks — 从 WordPress 或其他来源触发导入任务。

服务器自动发现并注册您在项目中配置的所有插件暴露的功能。

启动 MCP 服务器

npx emdash mcp

默认情况下,它在 http://localhost:4321/_emdash/mcp 上运行。服务器使用标准 MCP 传输,并且可以轻松地与 Claude Code、Cursor 或任何其他与 MCP 兼容的代理集成。

相关技能