Buenos candidatos
Campos personalizados, complementos de SEO, procesadores de contenido, extensiones de la interfaz de administración, analíticas, compartir en redes sociales, formularios
Muchos complementos de WordPress pueden migrarse a EmDash. El modelo de complemento es diferente—TypeScript en lugar de PHP, hooks en lugar de acciones/filtros, almacenamiento estructurado en lugar de wp_options—pero la mayoría de las funcionalidades se mapean de forma clara.
No todos los complementos tienen sentido migrar. Evalúa los candidatos antes de comenzar.
Buenos candidatos
Campos personalizados, complementos de SEO, procesadores de contenido, extensiones de la interfaz de administración, analíticas, compartir en redes sociales, formularios
Malos candidatos
Funciones multisitio, integraciones de WooCommerce/Gutenberg, complementos que parchean el núcleo interno de 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 usa add_action() y add_filter() con nombres de hooks de texto. EmDash usa hooks tipados declarados en la definición del complemento.
| WordPress | EmDash | Notas |
|---|---|---|
register_activation_hook() | plugin:install | Se ejecuta una vez en la primera instalación |
| Plugin habilitado | plugin:activate | Se ejecuta cuando se habilita |
| Plugin deshabilitado | plugin:deactivate | Se ejecuta cuando se deshabilita |
register_uninstall_hook() | plugin:uninstall | event.deleteData indica la elección del usuario |
| WordPress | EmDash | Notas |
|---|---|---|
wp_insert_post_data | content:beforeSave | Devuelve contenido modificado o lanza una excepción para cancelar |
save_post | content:afterSave | Efectos secundarios después de guardar |
before_delete_post | content:beforeDelete | Devuelve false para cancelar |
deleted_post | content:afterDelete | Limpieza después de la eliminación |
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 | Notas |
|---|---|---|
wp_handle_upload_prefilter | media:beforeUpload | Validar o transformar |
add_attachment | media:afterUpload | Reaccionar después de la carga |
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';
// Insertar$wpdb->insert($table, ['name' => 'Item 1', 'status' => 'active']);
// Consultar$items = $wpdb->get_results("SELECT \* FROM $table WHERE status = 'active' LIMIT 10");// Declare in plugin definitionstorage: { items: { indexes: ["status", "createdAt"], },},
// En hooks o rutas: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 usa la API de Configuración para formularios de administración. EmDash usa un esquema declarativo que genera automáticamente la interfaz de usuario.
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, }, },}Las páginas de administración de WordPress son PHP. EmDash usa componentes 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>Total de elementos: {count}</div>; },};
export const pages = { settings: function SettingsPage() { // React component for settings page return <div>Settings content</div>; },};Registrar en la definición del complemento:
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 }; }, },},Las rutas están disponibles en /_emdash/api/plugins/{plugin-id}/{route-name}.
Analiza el complemento de WordPress
Documenta lo que hace: hooks, operaciones de base de datos, páginas de administración, endpoints REST.
Mapea a conceptos de EmDash
Hooks de WordPress → Hooks de EmDash. wp_options → ctx.kv. Tablas personalizadas → Colecciones de almacenamiento. Páginas de administración → Componentes React. Endpoints REST → Rutas del complemento.
Crea el esqueleto del complemento
import { definePlugin } from "emdash";
export function createPlugin() { return definePlugin({ id: "my-ported-plugin", version: "1.0.0", capabilities: [], storage: {}, hooks: {}, routes: {}, admin: {}, });}Implementa en orden
Almacenamiento → Hooks → Interfaz de administración → Rutas
Prueba exhaustivamente
Verifica que los hooks se disparen correctamente, que el almacenamiento funcione y que la interfaz de administración se renderice.
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 }; }, }, });}Los plugins deben declarar las capacidades requeridas para el sandbox de seguridad:
| Capacidad | Proporciona | Caso de Uso |
|---|---|---|
network:fetch | ctx.http.fetch() | Llamadas a API externas |
read:content | ctx.content.get(), list() | Lectura de contenido del CMS |
write:content | ctx.content.create(), etc. | Modificación de contenido |
read:media | ctx.media.get(), list() | Lectura de medios |
write:media | ctx.media.getUploadUrl() | Subida de medios |
Sin estado global — Usa almacenamiento en lugar de variables globales.
Todo asíncrono — Siempre usa await en llamadas de almacenamiento y API.
Sin SQL directo — Usa colecciones de almacenamiento estructurado.
Sin sistema de archivos — Usa la API de medios para archivos.