creating-plugins

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Creating EmDash plugins

创建 EmDash 插件

Build against the API that reaches the intended execution mode. Source types and production-boundary tests take precedence over examples in this skill when they disagree.
针对能够触达目标执行模式的 API 进行开发。当本 skill 中的示例与源码类型或生产边界测试不一致时,应以后两者为准。

Choose a format

选择格式

FormatRuntime sourceAdmin UIDistribution
Sandboxed
src/plugin.ts
default-exports a
SandboxedPlugin
Block Kit pages, widgets, saved-entry panels, and actionsPlugin CLI and registry
Native
definePlugin()
/
createPlugin()
React, Block Kit, and Astro componentsTrusted site dependency only
Use a sandboxed plugin unless the feature needs host-process access, React admin code, Astro rendering components, raw page fragments, or custom Portable Text block definitions. Native plugins run with the site's authority and cannot be installed from the registry.
格式运行时源码管理 UI分发方式
沙箱化
src/plugin.ts
默认导出一个
SandboxedPlugin
Block Kit 页面、小组件、已保存条目面板和操作插件 CLI 和注册表
原生
definePlugin()
/
createPlugin()
React、Block Kit 和 Astro 组件仅可作为受信任的站点依赖
除非功能需要宿主进程访问权限、React 管理端代码、Astro 渲染组件、原始页面片段或自定义 Portable Text 块定义,否则应使用沙箱化插件。原生插件以站点权限运行,且无法从注册表安装。

Scaffold a sandboxed plugin

搭建沙箱化插件

sh
pnpm dlx @emdash-cms/plugin-cli init my-plugin
cd my-plugin
pnpm install
pnpm run test
The manifest is the identity and trust contract. Runtime hooks and routes live in
src/plugin.ts
; the CLI generates descriptors, manifests, and bundles. Do not create a separate descriptor factory or
sandbox-entry.ts
.
typescript
import type { SandboxedPlugin } from "emdash/plugin";

const plugin: SandboxedPlugin = {
	hooks: {
		"content:afterSave": async (event, ctx) => {
			ctx.log.info("Content saved", { id: event.content.id });
		},
	},
};

export default plugin;
Import authoring types from
emdash/plugin
with
import type
. Value imports are limited to lightweight helpers such as
pluginRoute()
and
pluginResponse()
, which the CLI bundles. Sandboxed runtime code can use Web APIs but not Node.js built-ins.
sh
pnpm dlx @emdash-cms/plugin-cli init my-plugin
cd my-plugin
pnpm install
pnpm run test
清单(manifest)是身份标识和信任契约。运行时 hooks 和 routes 位于
src/plugin.ts
中;CLI 会生成 descriptor、manifest 和 bundle。不要创建独立的 descriptor factory 或
sandbox-entry.ts
。
typescript
import type { SandboxedPlugin } from 'emdash/plugin';

const plugin: SandboxedPlugin = {
	hooks: {
		"content:afterSave": async (event, ctx) => {
			ctx.log.info("Content saved", { id: event.content.id });
		},
	},
};

export default plugin;
使用
import type
从
emdash/plugin
导入创作时类型。值导入仅限于
pluginRoute()
和
pluginResponse()
等轻量 helper,这些 helper 会被 CLI 打包。沙箱化运行时代码可以使用 Web APIs,但不能使用 Node.js 内置模块。

Declare access

声明访问权限

Declare every host API in
emdash-plugin.jsonc
. Adding authority, exposing a route publicly, or adding MCP tools requires renewed administrator approval.
CapabilityGrants
schema:read
Public collection and field definitions
admin.editor-draft:read
Selected unsaved field values after an explicit editor interaction
admin.editor-draft:patch
Host-validated unsaved field changes proposed for editor review
content:read
Content identity, translations, and published public URLs
content:revisions:read
Retained revision data; implies content read
content:write
Create, update, delete, and translation creation; implies read
content:publish
Revision-fenced publish, unpublish, schedule, and unschedule; implies read
content:restore
Revision-fenced reads and restoration of trashed content
hooks.content-policy:register
Pre-publish, pre-schedule, and pre-unpublish policy hooks
taxonomies:read
Taxonomy definitions, terms, and entry assignments
taxonomies:write
Term creation and assignment deltas; implies read
bylines:read
Public byline profiles and single or batched entry credits
redirects:read
Versioned redirect inspection
redirects:write
Versioned redirect creation, update, and deletion; implies read
comments:read
Stored non-trashed comments and their personal data
comments:moderate
Expected-status moderation; implies read
media:read
Ready-media metadata and authenticated asset URLs
media:bytes:read
Bounded media bytes and content hashes
media:metadata:write
Alt text, caption, and focal-point updates
media:write
Upload and delete; implies media read
network:request
ctx.http.fetch()
restricted to
allowedHosts
network:request:unrestricted
ctx.http.fetch()
without a host list
users:read
User directory lookup; also required by comment hooks
email:send
Email delivery when a transport is configured
hooks.email-transport:register
Exclusive
email:deliver
hook
hooks.email-events:register
Email before/after hooks
hooks.page-fragments:register
Trusted-only page fragments; excluded from sandbox registration
Settings, KV, declared storage, logging, and cron scheduling are plugin-scoped and need no capability. Use
ctx.settings
for user configuration,
ctx.kv
for internal key-value state, and declared
ctx.storage.<collection>
for queryable records.
Read the focused reference for the API being used:
  • Content, schema, translations, and publication
  • Taxonomies and redirects
  • Comments
  • Media
  • Storage, KV, and encrypted settings
必须在
emdash-plugin.jsonc
中声明每一个宿主 API。增加权限、公开路由或添加 MCP 工具,都需要管理员重新批准。
能力授予的权限
schema:read
公共集合和字段定义
admin.editor-draft:read
明确进行编辑器交互后所选中的未保存字段值
admin.editor-draft:patch
由宿主验证、供编辑器审核的未保存字段变更建议
content:read
内容标识、翻译和已发布的公共 URL
content:revisions:read
保留的修订数据;隐含 content read 权限
content:write
创建、更新、删除和创建翻译;隐含 read 权限
content:publish
受修订版本约束的发布、取消发布、定时发布和取消定时发布;隐含 read 权限
content:restore
受修订版本约束的已移入回收站内容读取和恢复
hooks.content-policy:register
发布前、定时发布前和取消发布前的策略 hooks
taxonomies:read
分类法定义、term 和条目分配
taxonomies:write
创建 term 及分配变更;隐含 read 权限
bylines:read
公开署名资料以及单条或批量条目的署名信息
redirects:read
版本化重定向检查
redirects:write
版本化重定向的创建、更新和删除;隐含 read 权限
comments:read
已存储且未移入回收站的评论及其个人数据
comments:moderate
带预期状态的评论审核;隐含 read 权限
media:read
已就绪媒体元数据和经认证的资源 URL
media:bytes:read
有大小上限的媒体字节和内容哈希
media:metadata:write
替代文本、题注和焦点位置更新
media:write
上传和删除;隐含 media read 权限
network:request
将
ctx.http.fetch()
限制在
allowedHosts
内
network:request:unrestricted
ctx.http.fetch()
不受 host 列表限制
users:read
用户目录查询;comment hooks 也需要此权限
email:send
已配置 transport 时的邮件发送
hooks.email-transport:register
独占
email:deliver
hook
hooks.email-events:register
邮件发送前/后的 hooks
hooks.page-fragments:register
仅限受信任的页面片段;沙箱化插件无法注册
设置、KV、已声明的 storage、日志记录和 cron 调度均属于插件作用域,无需 capability。使用
ctx.settings
进行用户配置,使用
ctx.kv
保存内部键值状态,使用已声明的
ctx.storage.<collection>
保存可查询记录。
针对所使用的 API,阅读相应的专项参考文档:
  • 内容、schema、翻译和发布
  • 分类法与重定向
  • 评论
  • 媒体
  • Storage、KV 和加密设置

Routes, HTTP, and MCP

路由、HTTP 和 MCP

Routes are private by default and require authentication, their declared RBAC permission, token scope or CSRF as appropriate. Public routes are internet-facing and require explicit install consent.
Undeclared routes keep the legacy JSON/query envelope. Declare methods and body modes for host-enforced parsing. Use
pluginResponse()
only on a route that declares a raw response. Outbound
ctx.http.fetch()
responses and declared route bodies are buffered and bounded.
MCP tools reference private JSON routes with explicit permissions and Zod input schemas. Mark difficult-to-reverse operations
destructive: true
. Read API routes and MCP tools.
路由默认私有,并需要身份验证、其声明的 RBAC 权限,以及适用情况下的 token scope 或 CSRF。公共路由面向互联网,且必须获得明确的安装同意。
未声明的路由保留旧版 JSON/query 封装。请声明方法和 body 模式,以使用宿主强制执行的解析逻辑。仅在路由声明了原始响应时使用
pluginResponse()
。出站
ctx.http.fetch()
响应和已声明路由的 body 均会经过缓冲,并受到大小限制。
MCP 工具通过显式权限引用私有 JSON 路由,并使用 Zod 输入 schema。将难以撤销的操作标记为
destructive: true
。请阅读 API 路由和 MCP 工具。

Hooks

Hooks

Sandboxed hooks enter the same priority, dependency, timeout, error-policy, enablement, exclusive-provider, and capability pipeline as trusted hooks. Publication policy hooks identify action origin and actor without granting publication authority. Comment moderation invoked through
ctx.comments
reports plugin origin and runs the normal after-hook once. Read Hooks.
沙箱化 hooks 会进入与受信任 hooks 相同的优先级、依赖、超时、错误策略、启用机制、独占提供者和 capability 处理管线。发布策略 hooks 会标识操作来源和执行者,但不会授予发布权限。通过
ctx.comments
调用的评论审核会标明插件来源,并运行一次正常的 after-hook。请阅读 Hooks。

Declarative admin UI

声明式管理 UI

Sandboxed pages and widgets return validated Block Kit. Structured links use host-resolved targets;
routeCtx.ui
carries host-attested locale, direction, and surface. External images require matching network authority.
Saved-entry panels and actions point to private routes. Ordinary panel load receives only host-reloaded saved identity and version. Add
admin.editor-draft:read
or
admin.editor-draft:patch
plus extension-level collection and field selectors when an explicit interaction must receive selected unsaved fields or propose an atomic whole-field patch. Patch does not imply read. The host previews accepted patches, marks the form dirty, and never saves them automatically. Read saved content through capability-gated
ctx.content
.
Declarative field widgets currently compose supported Block Kit elements into a JSON value. Custom Portable Text blocks and Astro render components remain native-only. Read Admin UI, Block Kit, and Portable Text blocks.
沙箱化页面和小组件返回经过验证的 Block Kit。结构化链接使用由宿主解析的目标;
routeCtx.ui
携带经宿主认证的语言区域(locale)、方向(direction)和呈现界面(surface)。外部图片需要匹配的网络权限。
已保存条目面板和操作指向私有路由。普通面板加载仅接收由宿主重新加载的已保存内容标识和版本。当明确交互必须接收选定的未保存字段或提出原子化的整字段 patch 时,需添加
admin.editor-draft:read
或
admin.editor-draft:patch
capability,以及扩展级 collection 和 field selector。Patch 不隐含 read 权限。宿主会预览接受的 patch,将表单标记为 dirty,且绝不会自动保存。通过受 capability 控制的
ctx.content
读取已保存内容。
声明式字段小组件目前将受支持的 Block Kit 元素组合成 JSON 值。自定义 Portable Text 块和 Astro 渲染组件仍仅限原生插件。请阅读 管理 UI、Block Kit 和 Portable Text 块。

Runner parity

运行器一致性

Cloudflare Worker Loader and Node/workerd execute the same bundle behind a plugin-scoped bridge. Both return buffered WHATWG responses from
ctx.http.fetch()
with binary bytes preserved and decoded request/response bodies limited to 8 MiB. Write against exported types, not extra methods found in one wrapper.
Read Sandbox boundaries before designing around an API not listed here.
Cloudflare Worker Loader 与 Node/workerd 通过插件作用域的 bridge 执行同一 bundle。两者均从
ctx.http.fetch()
返回缓冲的 WHATWG response,并保留二进制字节;已解码的请求/响应 body 限制为 8 MiB。请针对导出的类型编写代码,不要依赖某个 wrapper 中才有的额外方法。
在围绕本文未列出的 API 进行设计之前,请阅读 沙箱边界。

Test the production boundary

测试生产边界

Use
createPluginTestHost()
for fast hook, route, manifest, capability, KV, settings, and storage transport tests. Use
createPluginRuntimeTestHost()
when the test must exercise real content actions, plugin activation, media, comments, redirects, scheduling, restart, authorization, CSRF, caching, Block Kit validation, or saved-entry extensions.
Runtime fixtures establish state without firing hooks. Runtime actions call production boundaries; inspectors read observable state. Queue outbound HTTP responses with
host.http.respond()
and inspect requests with
host.http.requests()
. Dispose every host after use. Add Node/workerd parity only for runner-sensitive behavior.
使用
createPluginTestHost()
快速测试 hook、route、manifest、capability、KV、settings 和 storage transport。当测试必须覆盖真实内容操作、插件激活、媒体、评论、重定向、调度、重启、授权、CSRF、缓存、Block Kit 验证或已保存条目扩展时,使用
createPluginRuntimeTestHost()
。
运行时 fixtures 在不触发 hooks 的情况下建立状态。运行时 actions 调用生产边界;inspectors 读取可观察状态。使用
host.http.respond()
将出站 HTTP 响应加入队列,并使用
host.http.requests()
检查请求。使用后释放每个 host。仅当行为对运行器敏感时,才添加 Node/workerd 一致性测试。

References

参考文档

  • Content, schema, translations, and publication
  • Taxonomies and redirects
  • Comments
  • Media
  • Hooks
  • Storage, KV, and encrypted settings
  • Admin UI and field widgets
  • API routes and MCP tools
  • Block Kit
  • Portable Text blocks
  • Sandbox boundaries
  • Publishing
  • 内容、schema、翻译和发布
  • 分类法与重定向
  • 评论
  • 媒体
  • Hooks
  • Storage、KV 和加密设置
  • 管理 UI 和字段组件
  • API 路由和 MCP 工具
  • Block Kit
  • Portable Text 块
  • 沙箱边界
  • 发布