移植 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 使用的描述符 factory,以及以 definePlugin() 建構的執行階段 factory。選用的 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 變更。

下一步