このサイトは非公式の日本語訳です。Cloudflare・EmDashプロジェクトが運営するサイトではありません。

このページで分かること

  • EmDashに移植する候補になるプラグインと、AstroやEmDashで置き換わるため移植しないプラグインの見分け方
  • サンドボックス型とネイティブ型のパッケージの形式の違い
  • WordPressのフック、オプション、独自テーブル、RESTルート、管理画面の対応づけと、移植の手順
難易度
上級
読む時間
5分
このページの目次

WordPressのプラグインを移植するには、コンテンツに関する動作、保存データ、HTTPのルート、ユーザーインターフェースを切り分けます。そのうえで、それらの要件に対応するEmDashのプラグインの形式を選びます。

EmDashに移植する対象かの確認

移植の候補になるのは、コンテンツの検証、外部APIの呼び出し、バックグラウンド処理、独自の保存レコード、設定、管理画面のツールなど、WordPressのコアに依存しない動作を持つプラグインです。

AstroやEmDashがすでに置き換えている、WordPress固有の事柄を実装するだけのプラグインは移植しません。たとえば、PHPのページキャッシュ、WordPressのリライトルール、テーマのテンプレートの選択、WordPressのコアのグローバル変数の変更などです。

カスタム投稿タイプやフィールドを定義するだけで、実行時の動作がほとんどないプラグインは、プラグインではなく、EmDashのコレクションとシードファイルを作成します。

本サイトの補足 やさしい解説

WordPressのプラグインのすべてを、EmDashのプラグインに作り直す必要はありません。ページのキャッシュやURLのリライトのように、AstroやEmDashがすでに担っている機能は移植の対象外です。カスタム投稿タイプを作るだけのプラグイン(ACFでフィールドを足すだけ、など)は、EmDashではコレクションとシードファイルで置き換えます。移植するのは、外部APIの呼び出しや独自のデータの保存など、サイト固有の動作を持つものです。

サンドボックス型とネイティブ型の選択

まずプラグイン形式の選び方を読みます。2つの形式はフックの名前と PluginContext のAPIが共通ですが、ソースのパッケージは異なります。

要件 サンドボックス型 ネイティブ型
レジストリからのインストール 不可
分離された実行環境 可(ランナーを設定した場合) 不可
フック、ルート、KV、構造化ストレージ
Block Kitの管理画面のページ
独自のReactの管理画面のコンポーネント 不可
公開側の描画のためのAstroのコンポーネント 不可
そのままのページフラグメント 不可

移植にネイティブ型でしか使えないビルド時の仕組みやユーザーインターフェースが必要な場合にだけ、ネイティブ型を選びます。

サンドボックス型のパッケージの形式

emdash-plugin init で、現在のサンドボックス型の形式が作成されます。

Sandboxed plugin
my-plugin/
├── emdash-plugin.jsonc
├── src/
│   └── plugin.ts
├── tests/
│   └── plugin.test.ts
├── package.json
└── tsconfig.json

マニフェストには、識別情報、公開者、権限(Capability)、許可するホスト、ストレージの宣言が含まれます。バージョンは通常、package.json から取得されます。

次のマニフェストは、インデックス付きのストレージのコレクションを1つと、content:afterSave に必要な権限を宣言しています。

emdash-plugin.jsonc
{
  "$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 で型付けしたプレーンなオブジェクトを既定のエクスポートにします。サンドボックス型のフックのハンドラーは { handler } を使い、サンドボックス型のルートのハンドラーは (routeCtx, ctx) を受け取ります。

src/plugin.ts
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、ビルドの出力、サイトへの登録については、はじめてのサンドボックス型プラグインを参照してください。

ネイティブ型のパッケージの形式

ネイティブ型のパッケージは、astro.config.mjs 用の記述子のファクトリー関数と、definePlugin() で作る実行時のファクトリー関数の両方をエクスポートします。任意の管理画面用とAstro用のエントリーポイントは、パッケージの別のエクスポートにします。

Native plugin
my-native-plugin/
├── src/
│   ├── index.ts
│   ├── admin.tsx
│   └── astro/
│       └── index.ts
├── package.json
└── tsconfig.json

次の簡略化したネイティブ型のエントリーポイントは、必須の2つの部分を示しています。

src/index.ts
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;

ネイティブ型のフックのハンドラーは、関数を直接指定できます。ネイティブ型のルートのハンドラーは、1つにまとめられたコンテキストの引数を受け取ります。記述子と実行時の両方にある idversion、権限、エントリーポイントは、一致させておきます。

Reactの管理画面のページ、Portable Textの描画コンポーネント、ページフラグメントを追加する前に、はじめてのネイティブ型プラグインを読みます。

WordPressの動作の対応づけ

フック

WordPressのアクションやフィルターは、名前だけでなく、目的に合わせて対応づけます。

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

フックのイベントには、それぞれ型の付いた独自の形があります。WordPressのコールバックの引数を置き換える前に、フックのリファレンスを確認します。

エントリーのデータを受け取るコンテンツのフックには、content:read が必要です。移植したプラグインが呼び出すAPIに合わせて、権限を追加します。

権限 使えるようになるAPI
content:read コンテンツの読み取りと、エントリーのデータを渡すコンテンツのフックの登録
content:write コンテンツの作成、更新、公開、削除。コンテンツの読み取りも含みます
media:read メディアのレコードの読み取り
media:write メディアの作成と更新。メディアの読み取りも含みます
network:request allowedHosts に記載したホストに対する ctx.http の使用
本サイトの補足 やさしい解説

WordPressの save_postadd_attachment などのフックには、EmDashに目的の近いフックがあります(例:save_postcontent:afterSave)。ただし、EmDashのフックに渡されるデータの形はWordPressとは違うため、名前を置き換えるだけでは動きません。また、EmDashのプラグインは、使うAPIに応じた権限(content:read など)を宣言しておく必要があります。

オプションと独自テーブル

プラグインごとの小さな値には、ctx.kv を使います。キーはプラグインごとに分離されます。ユーザーの設定には settings:、内部の状態には state: を使うのが慣例です。

クエリで取得するプラグインのレコードには、宣言した ctx.storage.<collection> のコレクションを使います。ストレージの宣言は、サンドボックス型プラグインでは emdash-plugin.jsonc に、ネイティブ型プラグインでは definePlugin() に書きます。プラグインのコードからEmDashのデータベースを開いたり、SQLに値を埋め込んだりしないでください。

次の比較は、WordPressのグローバル変数を新しいプラグインに持ち込まずに、1つのオプションの値を移植する例です。

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.kv.set("settings:apiKey", newApiKey);
}

export async function readApiKey(ctx: PluginContext) {
  return await ctx.kv.get<string>("settings:apiKey") ?? "";
}

WordPressの独自テーブルの場合は、ストレージを宣言する前に、絞り込みと並べ替えに使うフィールドを特定します。次のサンドボックス型のマニフェストの一部は、クエリで使う2つのフィールドの両方をインデックスにしています。

"storage": {
  "jobs": { "indexes": ["status", "createdAt"] }
}

これで、実行時にジョブのレコードを保存し、クエリで取得できます。

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,
});

このクエリを実行する前に、マニフェストまたはネイティブ型のストレージの定義で、statuscreatedAt の両方をインデックスとして宣言します。

RESTエンドポイント

WordPressのRESTルートは、プラグインのルートに対応づけます。EmDashは、それを /_emdash/api/plugins/<plugin-id>/<route-name> に配置します。ルートが入力を受け付ける場合は inputSchema を定義し、JSONにシリアライズできるデータを返します。

設定と管理画面のページ

サンドボックス型プラグインは、Block Kitで管理画面のページを記述し、ルートとKVを通して値を読み書きします。管理画面のアプリケーションにReactを持ち込むことはありません。

ネイティブ型プラグインは、admin.settingsSchema を使って、フォームを自動生成できます。独自のReactのページ、ウィジェット、フィールドのウィジェット、一覧の列には、パッケージの adminEntry のエクスポートを使います。

ファイルとメディア

アップロードされたファイルや生成したファイルには、メディアのAPIを使います。サンドボックス型プラグインは、ファイルシステムにアクセスできません。ネイティブ型プラグインはホストのプロセスを共有しますが、デプロイ先のローカルにファイルを書き込むのは、環境を移しても使えるストレージの方法ではありません。

プラグインの移植

  1. WordPressのフック、オプション、独自テーブル、cronのジョブ、RESTルート、管理画面のページ、ブロック、ショートコード、外部のホストを洗い出します。

  2. Astroのルーティング、EmDashのコンテンツモデル、デプロイ先のプラットフォームが担う動作を取り除きます。

  3. サンドボックス型とネイティブ型のどちらのパッケージの形式にするかを選びます。残った動作に必要なすべての権限と許可するホストを記録します。

  4. KVのキーと、構造化ストレージのコレクションを定義します。whereorderBy で使うすべてのフィールドにインデックスを追加します。

  5. 目に見える動作を1つずつ移植します。代表的なコンテンツと失敗する場合で、フックやルートをテストします。

  6. Block Kitやネイティブ型の管理画面のUIは、土台になるルートとストレージの動作ができてから追加します。

  7. インストール、アップグレード、有効化、無効化、データを削除する場合と削除しない場合のアンインストール、権限の変更をテストします。

次のステップ