Gute Kandidaten
Benutzerdefinierte Felder, SEO-Plugins, Content-Prozessoren, Admin-UI-Erweiterungen, Analytics, Social Sharing, Formulare
Viele WordPress-Plugins können zu EmDash portiert werden. Das Plugin-Modell ist anders – TypeScript statt PHP, Hooks statt Actions/Filter, strukturierter Speicher statt wp_options – aber die meisten Funktionen lassen sich sauber abbilden.
Nicht alle Plugins sind sinnvoll zu portieren. Bewerten Sie Kandidaten, bevor Sie beginnen.
Gute Kandidaten
Benutzerdefinierte Felder, SEO-Plugins, Content-Prozessoren, Admin-UI-Erweiterungen, Analytics, Social Sharing, Formulare
Schlechte Kandidaten
Multisite-Funktionen, WooCommerce-/Gutenberg-Integrationen, Plugins, die WordPress-Core-Interna patchen
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 verwendet add_action() und add_filter() mit String-Hook-Namen. EmDash verwendet typisierte Hooks, die in der Plugin-Definition deklariert werden.
| WordPress | EmDash | Anmerkungen |
|---|---|---|
register_activation_hook() | plugin:install | Läuft einmal bei der ersten Installation |
| Plugin aktiviert | plugin:activate | Läuft bei Aktivierung |
| Plugin deaktiviert | plugin:deactivate | Läuft bei Deaktivierung |
register_uninstall_hook() | plugin:uninstall | event.deleteData zeigt Benutzerauswahl an |
| WordPress | EmDash | Anmerkungen |
|---|---|---|
wp_insert_post_data | content:beforeSave | Modifizierten Inhalt zurückgeben oder abbrechen |
save_post | content:afterSave | Nebeneffekte nach dem Speichern |
before_delete_post | content:beforeDelete | false zurückgeben, um abzubrechen |
deleted_post | content:afterDelete | Bereinigung nach Löschung |
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 | Anmerkungen |
|---|---|---|
wp_handle_upload_prefilter | media:beforeUpload | Validieren oder transformieren |
add_attachment | media:afterUpload | Nach Upload reagieren |
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';
// Einfügen$wpdb->insert($table, ['name' => 'Item 1', 'status' => 'active']);
// Abfrage$items = $wpdb->get_results("SELECT \* FROM $table WHERE status = 'active' LIMIT 10");// Declare in plugin definitionstorage: { items: { indexes: ["status", "createdAt"], },},
// In Hooks oder Routen: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 verwendet die Settings API für Admin-Formulare. EmDash verwendet ein deklaratives Schema, das automatisch die UI generiert.
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-Admin-Seiten sind PHP. EmDash verwendet React-Komponenten.
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>Gesamtanzahl Elemente: {count}</div>; },};
export const pages = { settings: function SettingsPage() { // React component for settings page return <div>Settings content</div>; },};In der Plugin-Definition registrieren:
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 }; }, },},Routen sind verfügbar unter /_emdash/api/plugins/{plugin-id}/{route-name}.
WordPress-Plugin analysieren
Dokumentieren Sie, was es tut: Hooks, Datenbankoperationen, Admin-Seiten, REST-Endpunkte.
Auf EmDash-Konzepte abbilden
WordPress-Hooks → EmDash-Hooks. wp_options → ctx.kv. Benutzerdefinierte Tabellen → Storage Collections. Admin-Seiten → React-Komponenten. REST-Endpunkte → Plugin-Routen.
Plugin-Grundgerüst erstellen
import { definePlugin } from "emdash";
export function createPlugin() { return definePlugin({ id: "my-ported-plugin", version: "1.0.0", capabilities: [], storage: {}, hooks: {}, routes: {}, admin: {}, });}In Reihenfolge implementieren
Speicher → Hooks → Admin-UI → Routen
Gründlich testen
Überprüfen Sie, ob Hooks korrekt ausgelöst werden, der Speicher funktioniert und die Admin-UI gerendert wird.
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 }; }, }, });}Plugins müssen aus Sicherheitsgründen für die Sandbox erforderliche Fähigkeiten deklarieren:
| Fähigkeit | Bietet Zugriff auf | Anwendungsfall |
|---|---|---|
network:fetch | ctx.http.fetch() | Externe API-Aufrufe |
read:content | ctx.content.get(), list() | Lesen von CMS-Inhalten |
write:content | ctx.content.create(), etc. | Ändern von Inhalten |
read:media | ctx.media.get(), list() | Lesen von Medien |
write:media | ctx.media.getUploadUrl() | Hochladen von Medien |
Kein globaler Zustand — Verwende Speicher statt globaler Variablen.
Alles asynchron — Warte immer mit await auf Speicher- und API-Aufrufe.
Kein direkter SQL — Verwende strukturierte Speichersammlungen.
Kein Dateisystem — Verwende die Media-API für Dateien.