多くのWordPressプラグインはEmDashに移植できます。プラグインモデルは異なります—PHPの代わりにTypeScript、アクション/フィルタの代わりにフック、wp_optionsの代わりに構造化ストレージ—ですが、ほとんどの機能はきれいにマッピングされます。
移植可能性の評価
すべてのプラグインを移植する意味があるわけではありません。開始前に候補を評価してください。
良い候補
カスタムフィールド、SEOプラグイン、コンテンツプロセッサ、管理UI拡張、アナリティクス、ソーシャルシェア、フォーム
不適切な候補
マルチサイト機能、WooCommerce/Gutenberg統合、WordPressコア内部をパッチするプラグイン
プラグイン構造の比較
WordPress
wp-content/plugins/my-plugin/
├── my-plugin.php # プラグインヘッダーを含むメインファイル
├── includes/
│ ├── class-admin.php
│ └── class-api.php
└── admin/
└── js/ EmDash
my-plugin/
├── src/
│ ├── index.ts # プラグイン定義(definePlugin)
│ └── admin.tsx # 管理UIエクスポート(React)
├── package.json
└── tsconfig.json フックのマッピング
WordPressは文字列のフック名でadd_action()とadd_filter()を使用します。EmDashはプラグイン定義で宣言された型付きフックを使用します。
ライフサイクルフック
| WordPress | EmDash | 備考 |
|---|---|---|
register_activation_hook() | plugin:install | 初回インストール時に一度実行 |
| プラグイン有効化 | plugin:activate | 有効化時に実行 |
| プラグイン無効化 | plugin:deactivate | 無効化時に実行 |
register_uninstall_hook() | plugin:uninstall | event.deleteDataがユーザーの選択を示す |
コンテンツフック
| WordPress | EmDash | 備考 |
|---|---|---|
wp_insert_post_data | content:beforeSave | 変更されたコンテンツを返すか、エラーをスローしてキャンセル |
save_post | content:afterSave | 保存後の副作用 |
before_delete_post | content:beforeDelete | キャンセルするにはfalseを返す |
deleted_post | content:afterDelete | 削除後のクリーンアップ |
WordPress
add_action('save_post', function($post_id, $post, $update) {
if ($post->post_type !== 'product') return;
$price = get_post_meta($post_id, 'price', true);
if ($price > 1000) {
update_post_meta($post_id, 'is_premium', true);
}
}, 10, 3);
EmDash
hooks: {
"content:afterSave": async (event, ctx) => {
if (event.collection !== "products") return;
const price = event.content.price as number;
if (price > 1000) {
await ctx.kv.set(`premium:${event.content.id}`, true);
}
},
} メディアフック
| WordPress | EmDash | 備考 |
|---|---|---|
wp_handle_upload_prefilter | media:beforeUpload | 検証または変換 |
add_attachment | media:afterUpload | アップロード後に反応 |
ストレージのマッピング
Options API → KVストア
WordPress
$api_key = get_option('my_plugin_api_key', '');
update_option('my_plugin_api_key', 'abc123');
delete_option('my_plugin_api_key'); EmDash
const apiKey = await ctx.kv.get<string>("settings:apiKey") ?? "";
await ctx.kv.set("settings:apiKey", "abc123");
await ctx.kv.delete("settings:apiKey"); カスタムテーブル → ストレージコレクション
WordPress
global $wpdb;
$table = $wpdb->prefix . 'my_plugin_items';
// 挿入
$wpdb->insert($table, ['name' => 'Item 1', 'status' => 'active']);
// クエリ
$items = $wpdb->get_results(
"SELECT \* FROM $table WHERE status = 'active' LIMIT 10"
);
EmDash
// プラグイン定義で宣言
storage: {
items: {
indexes: ["status", "createdAt"],
},
},
// フックまたはルート内で:
await ctx.storage.items.put("item-1", {
name: "Item 1",
status: "active",
createdAt: new Date().toISOString(),
});
const result = await ctx.storage.items.query({
where: { status: "active" },
limit: 10,
}); 設定スキーマ
WordPressは管理フォームにSettings APIを使用します。EmDashはUIを自動生成する宣言的スキーマを使用します。
WordPress
add_action('admin_init', function() {
register_setting('my_plugin', 'my_plugin_api_key');
add_settings_section('main', 'Settings', null, 'my-plugin');
add_settings_field('api_key', 'API Key', function() {
$value = get_option('my_plugin_api_key');
echo '<input type="text" name="my_plugin_api_key"
value="' . esc_attr($value) . '">';
}, 'my-plugin', 'main');
}); EmDash
admin: {
settingsSchema: {
apiKey: {
type: "secret",
label: "APIキー",
description: "ダッシュボードからのAPIキー",
},
enabled: {
type: "boolean",
label: "有効",
default: true,
},
limit: {
type: "number",
label: "アイテム制限",
default: 100,
min: 1,
max: 1000,
},
},
} 管理UI
WordPressの管理ページはPHPです。EmDashはReactコンポーネントを使用します。
import { useState, useEffect } from "react";
export const widgets = {
summary: function SummaryWidget() {
const [count, setCount] = useState(0);
useEffect(() => {
fetch("/_emdash/api/plugins/my-plugin/status")
.then((r) => r.json())
.then((data) => setCount(data.count));
}, []);
return <div>合計アイテム: {count}</div>;
},
};
export const pages = {
settings: function SettingsPage() {
// 設定ページ用のReactコンポーネント
return <div>設定コンテンツ</div>;
},
};
プラグイン定義に登録:
admin: {
entry: "@my-org/my-plugin/admin",
pages: [{ path: "/settings", label: "ダッシュボード" }],
widgets: [{ id: "summary", title: "概要", size: "half" }],
},
REST API → プラグインルート
WordPress
register_rest_route('my-plugin/v1', '/items', [
'methods' => 'GET',
'callback' => function($request) {
global $wpdb;
$items = $wpdb->get_results("SELECT * FROM items LIMIT 50");
return new WP_REST_Response($items);
},
]); EmDash
routes: {
items: {
handler: async (ctx) => {
const result = await ctx.storage.items.query({ limit: 50 });
return { items: result.items };
},
},
}, ルートは/_emdash/api/plugins/{plugin-id}/{route-name}で利用可能です。
移植プロセス
-
WordPressプラグインを分析する
何をするかを文書化します:フック、データベース操作、管理ページ、RESTエンドポイント。
-
EmDashの概念にマッピングする
WordPressフック → EmDashフック。
wp_options→ctx.kv。カスタムテーブル → ストレージコレクション。管理ページ → Reactコンポーネント。RESTエンドポイント → プラグインルート。 -
プラグインのスケルトンを作成する
import { definePlugin } from "emdash"; export function createPlugin() { return definePlugin({ id: "my-ported-plugin", version: "1.0.0", capabilities: [], storage: {}, hooks: {}, routes: {}, admin: {}, }); } -
順番に実装する
ストレージ → フック → 管理UI → ルート
-
徹底的にテストする
フックが正しく起動し、ストレージが機能し、管理UIがレンダリングされることを確認します。
例:読書時間プラグイン
WordPress
add_filter('wp_insert_post_data', function($data, $postarr) {
if ($data['post_type'] !== 'post') return $data;
$content = strip_tags($data['post_content']);
$word_count = str_word_count($content);
$read_time = ceil($word_count / 200);
if (!empty($postarr['ID'])) {
update_post_meta($postarr['ID'], '_read_time', $read_time);
}
return $data;
}, 10, 2);
EmDash
export function createPlugin() {
return definePlugin({
id: "read-time",
version: "1.0.0",
admin: {
settingsSchema: {
wordsPerMinute: {
type: "number",
label: "1分あたりの単語数",
default: 200,
min: 100,
max: 400,
},
},
},
hooks: {
"content:beforeSave": async (event, ctx) => {
if (event.collection !== "posts") return;
const wpm = await ctx.kv.get<number>("settings:wordsPerMinute") ?? 200;
const text = JSON.stringify(event.content.body || "");
const readTime = Math.ceil(text.split(/\s+/).length / wpm);
return { ...event.content, readTime };
},
},
});
} 機能
プラグインはセキュリティサンドボックス化に必要な機能を宣言する必要があります:
| 機能 | 提供 | ユースケース |
|---|---|---|
network:request | ctx.http.fetch() | 外部API呼び出し |
content:read | ctx.content.get(), list() | CMSコンテンツの読み取り |
content:write | ctx.content.create(), etc. | コンテンツの変更 |
media:read | ctx.media.get(), list() | メディアの読み取り |
media:write | ctx.media.getUploadUrl() | メディアのアップロード |
よくある落とし穴
グローバル状態なし — グローバル変数の代わりにストレージを使用してください。
すべて非同期 — ストレージとAPI呼び出しには常にawaitを使用してください。
直接SQLなし — 構造化されたストレージコレクションを使用してください。
ファイルシステムなし — ファイルにはメディアAPIを使用してください。
次のステップ
- Hooks — シグネチャ付きのすべてのフック
- Storage — コレクションとクエリ
- Settings — Block Kit経由のKVベース設定
- React Admin Pages & Widgets — 管理ページの構築(ネイティブプラグイン)