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.
针对能够进入目标 execution mode 的 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.
格式运行时源Admin UI分发方式
沙箱化
src/plugin.ts
默认导出
SandboxedPlugin
Block Kit pages、widgets、saved-entry panels 和 actionsPlugin CLI 和注册表
原生
definePlugin()
/
createPlugin()
React、Block Kit 和 Astro components仅可作为受信任的站点依赖
除非该功能需要宿主进程访问权限、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
. The CLI bundles value imports such as
pluginRoute()
and
pluginResponse()
from
emdash/plugin
and
portableTextToMarkdown()
from
emdash/client
. 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 会生成 descriptors、manifests 和 bundles。不要创建单独的 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
导入编写插件时使用的类型。CLI 会打包值导入,例如来自
emdash/plugin
的
pluginRoute()
和
pluginResponse()
,以及来自
emdash/client
的
portableTextToMarkdown()
。沙箱运行时代码可以使用 Web APIs,但不能使用 Node.js built-ins。

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
中声明每一个 host API。新增权限范围、公开 route 或添加 MCP tools,都需要管理员重新批准。
Capability授予的权限
schema:read
公开的 collection 和 field 定义
admin.editor-draft:read
编辑器发生明确交互后获取所选的未保存 field 值
admin.editor-draft:patch
提交给编辑器审核、并由 host 验证的未保存 field 更改
content:read
Content 标识、translations 和已发布的公开 URL
content:revisions:read
保留的 revision 数据;隐含
content:read
权限
content:write
创建、更新、删除 content 以及创建 translation;隐含 read 权限
content:publish
受 revision 边界约束的 publish、unpublish、schedule 和 unschedule;隐含 read 权限
content:restore
受 revision 边界约束的已移入回收站 content 的读取与恢复
hooks.content-policy:register
Publish、schedule 和 unpublish 前的 policy hooks
taxonomies:read
Taxonomy 定义、terms 和 entry assignments
taxonomies:write
创建 terms 和 entry assignment 增量;隐含 read 权限
bylines:read
公开的 byline 资料以及单个或批量 entry credits
redirects:read
版本化 redirect 检查
redirects:write
版本化 redirect 的创建、更新和删除;隐含 read 权限
comments:read
已存储且未移入回收站的 comments 及其个人数据
comments:moderate
按预期状态执行 moderation;隐含 read 权限
media:read
已就绪 media 的 metadata 和经认证的 asset URLs
media:bytes:read
有大小限制的 media bytes 和 content hashes
media:metadata:write
Alt text、caption 和 focal point 更新
media:write
上传和删除;隐含
media:read
权限
network:request
ctx.http.fetch()
受
allowedHosts
限制
network:request:unrestricted
ctx.http.fetch()
不受 host list 限制
users:read
用户目录查询;comment hooks 也需要此权限
email:send
配置 transport 后进行 email delivery
hooks.email-transport:register
独占
email:deliver
hook
hooks.email-events:register
Email 发送前后的 hooks
hooks.page-fragments:register
仅限受信任的 page fragments;沙箱注册中不包含此项
Settings、KV、已声明的 storage、logging 和 cron scheduling 均限定在 plugin scope 内,不需要 capability。使用
ctx.settings
管理用户配置,使用
ctx.kv
管理内部 key-value 状态,使用已声明的
ctx.storage.<collection>
管理可查询的 records。
请阅读正在使用的 API 所对应的专项参考文档:
  • Content、schema、translations 与 publication
  • Taxonomies 与 redirects
  • Comments
  • Media
  • Storage、KV 与加密 settings

Routes, HTTP, and MCP

Routes、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.
Routes 默认是私有的,需要身份验证、其声明的 RBAC permission,以及根据适用情况提供 token scope 或 CSRF。公开 routes 面向互联网,需要明确的安装同意。
未声明的 routes 保留旧版 JSON/query envelope。声明 methods 和 body modes,以便 host 强制执行解析。仅在 route 明确声明 raw response 时使用
pluginResponse()
。出站
ctx.http.fetch()
responses 和已声明的 route bodies 都会经过缓冲并受到大小限制。
MCP tools 引用具有明确 permissions 和 Zod input schemas 的私有 JSON routes。将难以撤销的 operations 标记为
destructive: true
。请阅读 API routes 与 MCP tools。

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 会进入与 trusted hooks 相同的 priority、dependency、timeout、error-policy、enablement、exclusive-provider 和 capability pipeline。Publication policy hooks 会标识 action origin 和 actor,但不会授予 publication authority。通过
ctx.comments
调用的 comment moderation 会报告 plugin origin,并且只运行一次常规 after-hook。请阅读 Hooks。

Declarative admin UI

声明式 Admin 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.
沙箱 pages 和 widgets 返回经过验证的 Block Kit。结构化 links 使用由 host 解析的 targets;
routeCtx.ui
携带经 host 认证的 locale、direction 和 surface。外部 images 需要匹配的 network authority。
Saved-entry panels 和 actions 指向私有 routes。普通 panel load 只会收到由 host 重新加载的 saved identity 和 version。当明确交互必须获取所选的未保存 fields 或提出原子的 whole-field patch 时,添加
admin.editor-draft:read
或
admin.editor-draft:patch
,以及 extension 级别的 collection 和 field selectors。Patch 不隐含 read。Host 会预览接受的 patches,将 form 标记为 dirty,并且绝不会自动保存。通过受 capability 控制的
ctx.content
读取 saved content。
声明式 field widgets 当前将受支持的 Block Kit elements 组合成一个 JSON value。自定义 Portable Text blocks 和 Astro render components 仍仅适用于 native plugins。请阅读 Admin UI 与 field widgets、Block Kit 和 Portable Text blocks。

Runner parity

Runner 一致性

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 在 plugin-scoped bridge 之后执行同一 bundle。两者都会从
ctx.http.fetch()
返回经过缓冲的 WHATWG responses,保留 binary bytes,并将解码后的 request/response bodies 限制在 8 MiB 内。请依据 exported types 编写,不要依赖某一个 wrapper 中才存在的额外 methods。
在围绕本文未列出的 API 进行设计之前,请阅读 Sandbox boundaries。

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 测试。当测试必须涉及真实 content actions、plugin activation、media、comments、redirects、scheduling、restart、authorization、CSRF、caching、Block Kit validation 或 saved-entry extensions 时,使用
createPluginRuntimeTestHost()
。
Runtime fixtures 在不触发 hooks 的情况下建立 state。Runtime actions 调用 production boundaries;inspectors 读取 observable state。使用
host.http.respond()
将出站 HTTP responses 加入队列,并使用
host.http.requests()
检查 requests。每次使用后都要 dispose 所有 hosts。仅在行为对 runner 敏感时添加 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
  • Content、schema、translations 与 publication
  • Taxonomies 与 redirects
  • Comments
  • Media
  • Hooks
  • Storage、KV 与加密 settings
  • Admin UI 与 field widgets
  • API routes 与 MCP tools
  • Block Kit
  • Portable Text blocks
  • Sandbox boundaries
  • Publishing