移植 WordPress 插件

本页内容

移植 WordPress 插件时,应将其内容行为、存储数据、HTTP 路由与用户界面分开处理,然后选择满足这些需求的 EmDash 插件格式。

判断插件是否适合 EmDash

合适的插件通常拥有独立于 WordPress 核心的行为,例如内容校验、外部 API 调用、后台处理、自定义存储记录、设置或管理工具。

不要移植仅用于实现 Astro 或 EmDash 已替代之 WordPress 功能的插件。例如 PHP 页面缓存、WordPress 重写规则、主题模板选择或对 WordPress 核心全局变量的修改。

若插件只定义自定义文章类型或字段、几乎没有运行时行为,应改为创建 EmDash 集合与 seed 文件,而不是插件。

选择 sandboxed 或 native

从选择插件格式开始。两种格式共享 hook 名称与 PluginContext API,但源码包结构不同。

需求SandboxedNative
从 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 的意图,而不仅是名称:

WordPressEmDash
register_activation_hook()首次安装用 plugin:install,启用用 plugin:activate
register_uninstall_hook()plugin:uninstall
wp_insert_post_datacontent:beforeSave
save_postcontent:afterSave
before_delete_postcontent:beforeDelete
deleted_postcontent:afterDelete
wp_handle_upload_prefiltermedia:beforeUpload
add_attachmentmedia: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 插件与宿主进程共享,但写入部署本地文件并非可移植的存储策略。

移植插件

  1. 清点 WordPress hooks、选项、自定义表、cron、REST 路由、管理页、区块、短代码与外部主机。

  2. 移除属于 Astro 路由、EmDash 内容模型或部署平台的行为。

  3. 选择 sandboxed 或 native 包格式,并记录剩余行为所需的 capability 与允许的主机。

  4. 定义 KV 键与结构化存储集合。为 where 或 orderBy 使用的每个字段添加索引。

  5. 每次移植一种可观察的行为,用代表性内容与失败场景测试 hook 或路由。

  6. 仅在底层路由与存储可用后,再添加 Block Kit 或 native 管理 UI。

  7. 测试安装、升级、激活、停用、带/不带数据删除的卸载,以及 capability 变更。

下一步