移植 WordPress 插件时,应将其内容行为、存储数据、HTTP 路由与用户界面分开处理,然后选择满足这些需求的 EmDash 插件格式。
判断插件是否适合 EmDash
合适的插件通常拥有独立于 WordPress 核心的行为,例如内容校验、外部 API 调用、后台处理、自定义存储记录、设置或管理工具。
不要移植仅用于实现 Astro 或 EmDash 已替代之 WordPress 功能的插件。例如 PHP 页面缓存、WordPress 重写规则、主题模板选择或对 WordPress 核心全局变量的修改。
若插件只定义自定义文章类型或字段、几乎没有运行时行为,应改为创建 EmDash 集合与 seed 文件,而不是插件。
选择 sandboxed 或 native
从选择插件格式开始。两种格式共享 hook 名称与 PluginContext API,但源码包结构不同。
| 需求 | Sandboxed | Native |
|---|---|---|
| 从 Registry 安装 | 是 | 否 |
| 隔离运行时 | 是(需配置 runner) | 否 |
| hooks、路由、KV、结构化存储 | 是 | 是 |
| Block Kit 管理页 | 是 | 是 |
| 自定义 React 管理组件 | 否 | 是 |
| 用于公开渲染的 Astro 组件 | 否 | 是 |
| 原始页面片段 | 否 | 是 |
仅当移植需要 native 专属的构建时或 UI 能力时,才选择 native。
Sandboxed 包格式
emdash-plugin init 会创建当前的 sandboxed 格式:
my-plugin/
├── emdash-plugin.jsonc
├── src/
│ └── plugin.ts
├── tests/
│ └── plugin.test.ts
├── package.json
└── tsconfig.json
清单包含标识、publisher、capabilities、允许的主机与存储声明。版本通常来自 package.json。
以下清单声明了一个带索引的存储集合,以及 content:afterSave 所需的 capability:
{
"$schema": "./node_modules/@emdash-cms/plugin-cli/schemas/emdash-plugin.schema.json",
"slug": "read-time",
"publisher": "did:plc:abc123def456",
"license": "MIT",
"author": { "name": "Example Author" },
"security": { "email": "security@example.com" },
"capabilities": ["content:read"],
"allowedHosts": [],
"storage": {
"calculations": { "indexes": ["contentId", "updatedAt"] }
}
}
src/plugin.ts 默认导出 SandboxedPlugin 类型的 plain object。sandboxed hook 处理器使用 { handler };sandboxed 路由处理器接收 (routeCtx, ctx):
import type { SandboxedPlugin } from "emdash/plugin";
export default {
hooks: {
"content:afterSave": {
handler: async (event, ctx) => {
await ctx.storage.calculations.put(event.content.id, {
contentId: event.content.id,
updatedAt: new Date().toISOString(),
});
},
},
},
routes: {
recent: {
handler: async (_routeCtx, ctx) => {
const result = await ctx.storage.calculations.query({
orderBy: { updatedAt: "desc" },
limit: 10,
});
return { items: result.items };
},
},
},
} satisfies SandboxedPlugin;
该路由位于 /_emdash/api/plugins/read-time/recent。查询若要按某字段过滤或排序,须先将该存储字段声明为索引。
使用 emdash-plugin build 构建包;不要为此格式手写 src/index.ts 描述符。生成 package.json、构建输出与站点注册见你的第一个 sandboxed 插件。
Native 包格式
native 包同时导出供 astro.config.mjs 使用的描述符工厂,以及用 definePlugin() 构建的运行时工厂。可选的 admin 与 Astro 入口为独立包导出。
my-native-plugin/
├── src/
│ ├── index.ts
│ ├── admin.tsx
│ └── astro/
│ └── index.ts
├── package.json
└── tsconfig.json
以下精简 native 入口展示两个必需部分:
import { definePlugin } from "emdash";
import type { PluginDescriptor } from "emdash";
export interface ReadTimeOptions {
wordsPerMinute?: number;
}
export function readTimePlugin(options: ReadTimeOptions = {}): PluginDescriptor {
return {
id: "read-time",
version: "0.1.0",
format: "native",
entrypoint: "@example/plugin-read-time",
capabilities: ["content:read"],
options,
};
}
export function createPlugin(options: ReadTimeOptions = {}) {
return definePlugin({
id: "read-time",
version: "0.1.0",
capabilities: ["content:read"],
admin: {
settingsSchema: {
wordsPerMinute: {
type: "number",
label: "Words per minute",
default: options.wordsPerMinute ?? 200,
min: 1,
},
},
},
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved", { id: event.content.id });
},
},
});
}
export default createPlugin;
native hook 处理器可直接为函数。native 路由处理器接收一个合并的上下文参数。请保持描述符与运行时中 id、version、capabilities 与 entrypoint 一致。
在添加 React 管理页、Portable Text 渲染器或页面片段之前,请先阅读你的第一个 native 插件。
映射 WordPress 行为
Hooks
映射 WordPress action 或 filter 的意图,而不仅是名称:
| WordPress | EmDash |
|---|---|
register_activation_hook() | 首次安装用 plugin:install,启用用 plugin:activate |
register_uninstall_hook() | plugin:uninstall |
wp_insert_post_data | content:beforeSave |
save_post | content:afterSave |
before_delete_post | content:beforeDelete |
deleted_post | content:afterDelete |
wp_handle_upload_prefilter | media:beforeUpload |
add_attachment | media:afterUpload |
hook 事件有各自的类型形状。在翻译 WordPress 回调参数前,请查阅 hook 参考。
接收条目数据的内容 hook 需要 content:read。根据移植代码调用的 API 添加 capability:
| Capability | 可用的 API |
|---|---|
content:read | 读取内容,注册暴露条目数据的内容 hook |
content:write | 创建、更新、发布或删除内容(亦含读取) |
media:read | 读取媒体记录 |
media:write | 创建或更新媒体(亦含读取) |
network:request | 对 allowedHosts 中列出的主机使用 ctx.http |
选项与自定义表
用户配置使用 ctx.settings,小型内部值使用 ctx.kv。两者均按插件隔离。在 admin.settingsSchema 中将凭据声明为 secret 字段,以便 EmDash 加密。
可查询的插件记录使用已声明的 ctx.storage.<collection> 集合。sandboxed 的存储声明在 emdash-plugin.jsonc,native 在 definePlugin() 中。不要打开 EmDash 数据库,也不要在插件代码中拼接 SQL。
以下对比在不向新插件暴露 WordPress 全局变量的情况下移植一个选项值:
WordPress
$api_key = get_option('read_time_api_key', '');
update_option('read_time_api_key', $new_api_key); EmDash
import type { PluginContext } from "emdash/plugin";
export async function saveApiKey(ctx: PluginContext, newApiKey: string) {
await ctx.settings.set("apiKey", newApiKey);
}
export async function readApiKey(ctx: PluginContext) {
return await ctx.settings.get<string>("apiKey") ?? "";
} 对于 WordPress 自定义表,在声明存储前先确定用于过滤与排序的字段。以下 sandboxed 清单片段为查询使用的两个字段都建立了索引:
"storage": {
"jobs": { "indexes": ["status", "createdAt"] }
}
运行时即可存储并查询 job 记录:
await ctx.storage.jobs.put("job-123", {
status: "pending",
createdAt: new Date().toISOString(),
});
const pending = await ctx.storage.jobs.query({
where: { status: "pending" },
orderBy: { createdAt: "asc" },
limit: 50,
});
执行该查询前,请在清单或 native 存储定义中将 status 与 createdAt 均声明为索引。
REST 端点
将 WordPress REST 路由映射为插件路由。EmDash 将其挂载在 /_emdash/api/plugins/<plugin-id>/<route-name>。若路由接受输入,请定义 inputSchema,并返回可 JSON 序列化的数据。
设置与管理页
sandboxed 插件用 Block Kit 描述管理页,并通过路由与 KV 读写值。它们不会向管理应用交付 React。
native 插件可用 admin.settingsSchema 生成表单。自定义 React 页、小部件、字段小部件或列表列请使用包导出 adminEntry。
文件与媒体
上传或生成的文件请使用媒体 API。sandboxed 插件无法访问文件系统。native 插件与宿主进程共享,但写入部署本地文件并非可移植的存储策略。
移植插件
-
清点 WordPress hooks、选项、自定义表、cron、REST 路由、管理页、区块、短代码与外部主机。
-
移除属于 Astro 路由、EmDash 内容模型或部署平台的行为。
-
选择 sandboxed 或 native 包格式,并记录剩余行为所需的 capability 与允许的主机。
-
定义 KV 键与结构化存储集合。为
where或orderBy使用的每个字段添加索引。 -
每次移植一种可观察的行为,用代表性内容与失败场景测试 hook 或路由。
-
仅在底层路由与存储可用后,再添加 Block Kit 或 native 管理 UI。
-
测试安装、升级、激活、停用、带/不带数据删除的卸载,以及 capability 变更。
下一步
- Sandboxed 插件清单:信任契约与包元数据。
- Capabilities:内容、媒体、网络主机与 hooks 的访问。
- Storage:KV 与索引集合。
- React 管理页:native 专属 UI。