適した候補
カスタムフィールド、SEOプラグイン、コンテンツプロセッサー、管理パネルUI拡張、アナリティクス、ソーシャル共有、フォーム
多くのWordPressプラグインはEmDashに移植できます。プラグインモデルは異なります(PHPの代わりにTypeScript、アクション/フィルターの代わりにフック、wp_optionsの代わりに構造化ストレージ)が、ほとんどの機能はきれいにマッピングできます。
すべてのプラグインが移植に適しているわけではありません。開始前に候補を評価してください。
適した候補
カスタムフィールド、SEOプラグイン、コンテンツプロセッサー、管理パネルUI拡張、アナリティクス、ソーシャル共有、フォーム
不適な候補
マルチサイト機能、WooCommerce/Gutenberg統合、WordPressコア内部をパッチするプラグイン
wp-content/plugins/my-plugin/├── my-plugin.php # Main file with plugin header├── includes/│ ├── class-admin.php│ └── class-api.php└── admin/ └── js/my-plugin/├── src/│ ├── index.ts # Plugin definition (definePlugin)│ └── admin.tsx # Admin UI exports (React)├── package.json└── tsconfig.jsonWordPressは文字列フック名を使用した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 | 削除後のクリーンアップ |
phpadd_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);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 | アップロード後に反応 |
php$api_key = get_option('my_plugin_api_key', '');update_option('my_plugin_api_key', 'abc123');delete_option('my_plugin_api_key');const apiKey = await ctx.kv.get<string>("settings:apiKey") ?? "";await ctx.kv.set("settings:apiKey", "abc123");await ctx.kv.delete("settings:apiKey");phpglobal $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");// Declare in plugin definitionstorage: { 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を自動生成する宣言型スキーマを使用します。
phpadd_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');});admin: { settingsSchema: { apiKey: { type: "secret", label: "API Key", description: "Your API key from the dashboard", }, enabled: { type: "boolean", label: "Enabled", default: true, }, limit: { type: "number", label: "Item Limit", default: 100, min: 1, max: 1000, }, },}WordPressの管理パネルページはPHPです。EmDashはReactコンポーネントを使用します。
tsx title="src/admin.tsx"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 component for settings page return <div>Settings content</div>; },};プラグイン定義で登録:
typescript title="src/index.ts"admin: { entry: "@my-org/my-plugin/admin", pages: [{ path: "/settings", label: "Dashboard" }], widgets: [{ id: "summary", title: "Summary", size: "half" }],},phpregister_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); },]);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がレンダリングされるかを確認。
phpadd_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);typescript title="src/index.ts"export function createPlugin() { return definePlugin({ id: "read-time", version: "1.0.0",
admin: { settingsSchema: { wordsPerMinute: { type: "number", label: "Words per minute", 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:fetch | ctx.http.fetch() | 外部API呼び出し |
read:content | ctx.content.get(), list() | CMSコンテンツの読み取り |
write:content | ctx.content.create()など | コンテンツの変更 |
read:media | ctx.media.get(), list() | メディアの読み取り |
write:media | ctx.media.getUploadUrl() | メディアのアップロード |
グローバル状態なし — グローバル変数の代わりにストレージを使用してください。
すべてが非同期 — ストレージとAPI呼び出しは常にawaitしてください。
直接SQLなし — 構造化ストレージコレクションを使用してください。
ファイルシステムなし — ファイルにはメディアAPIを使用してください。