Portar plugins de WordPress

En esta página

Porte un plugin de WordPress separando su comportamiento de contenido, datos almacenados, rutas HTTP e interfaz de usuario. Luego elija el formato de plugin de EmDash que admita esos requisitos.

Comprobar si el plugin encaja en EmDash

Buenos candidatos implementan comportamiento independiente del núcleo de WordPress, como validación de contenido, llamadas a APIs externas, procesamiento en segundo plano, registros personalizados, ajustes o una herramienta de administración.

No porte un plugin cuyo único propósito sea implementar una función de WordPress que Astro o EmDash ya reemplazan. Ejemplos: caché de páginas PHP, reglas de reescritura de WordPress, selección de plantillas del tema o modificaciones a globales del núcleo.

Si un plugin solo define un tipo de entrada o campos personalizados con poco comportamiento en tiempo de ejecución, cree una colección y un archivo seed de EmDash en lugar de un plugin.

Elegir sandboxed o native

Empiece con Elegir un formato de plugin. Ambos formatos comparten nombres de hooks y las APIs de PluginContext, pero sus paquetes fuente son distintos.

RequisitoSandboxedNative
Instalación desde el registrySíNo
Runtime aisladoSí, con un runner configuradoNo
Hooks, rutas, KV, almacenamiento estructuradoSíSí
Páginas de admin con Block KitSíSí
Componentes React de admin personalizadosNoSí
Componentes Astro para renderizado públicoNoSí
Fragmentos de página sin procesarNoSí

Elija native solo cuando el port necesite una superficie de compilación o interfaz exclusiva de native.

Formato de paquete sandboxed

emdash-plugin init crea el formato sandboxed actual:

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

El manifiesto contiene identidad, publisher, capabilities, hosts permitidos y declaraciones de almacenamiento. La versión suele venir de package.json.

El siguiente manifiesto declara una colección de almacenamiento indexada y la capability requerida por content:afterSave:

{
  "$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 exporta por defecto un objeto plano tipado con SandboxedPlugin. Los handlers de hooks sandboxed usan { handler }; los handlers de rutas sandboxed reciben (routeCtx, ctx):

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;

La ruta está disponible en /_emdash/api/plugins/read-time/recent. Un campo de almacenamiento debe declararse como índice antes de que una consulta pueda filtrar u ordenar por él.

Compile el paquete con emdash-plugin build; no añada un descriptor src/index.ts escrito a mano a este formato. Lea Su primer plugin sandboxed para el package.json generado, la salida de build y el registro en el sitio.

Formato de paquete native

Un paquete native exporta una factory de descriptor para astro.config.mjs y una factory de runtime construida con definePlugin(). Los entrypoints opcionales de admin y Astro son exportaciones separadas del paquete.

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

El siguiente entrypoint native reducido muestra las dos piezas requeridas:

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;

Los handlers de hooks native pueden ser funciones directamente. Los handlers de rutas native reciben un único argumento de contexto combinado. Mantenga alineadas las copias de descriptor y runtime de id, version, capabilities y entrypoints.

Lea Su primer plugin native antes de añadir páginas React de admin, renderizadores Portable Text o fragmentos de página.

Mapear el comportamiento de WordPress

Hooks

Mapee la intención de una action o filter de WordPress, no solo el nombre:

WordPressEmDash
register_activation_hook()plugin:install en la primera instalación, o plugin:activate al habilitar
register_uninstall_hook()plugin:uninstall
wp_insert_post_datacontent:beforeSave
save_postcontent:afterSave
before_delete_postcontent:beforeDelete
deleted_postcontent:afterDelete
wp_handle_upload_prefiltermedia:beforeUpload
add_attachmentmedia:afterUpload

Los eventos de hooks tienen formas tipadas propias. Consulte la referencia de hooks antes de traducir argumentos de callbacks de WordPress.

Los hooks de contenido que reciben datos de entradas requieren content:read. Añada capabilities según las APIs que llame el port:

CapabilityAPI disponible
content:readLeer contenido y registrar hooks de contenido que exponen datos de entradas
content:writeCrear, actualizar, publicar o eliminar contenido; también implica lectura
media:readLeer registros de medios
media:writeCrear o actualizar medios; también implica lectura
network:requestUsar ctx.http para los hosts listados en allowedHosts

Opciones y tablas personalizadas

Use ctx.settings para la configuración del usuario y ctx.kv para valores internos pequeños. Ambos almacenes están aislados por plugin. Declare credenciales como campos secret en admin.settingsSchema para que EmDash las cifre.

Use colecciones ctx.storage.<collection> declaradas para registros consultables del plugin. La declaración de almacenamiento va en emdash-plugin.jsonc para sandboxed y en definePlugin() para native. No abra la base de datos de EmDash ni interpole SQL desde código del plugin.

La siguiente comparación porta un valor de opción sin exponer globales de WordPress al nuevo plugin:

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

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

Para una tabla personalizada de WordPress, identifique los campos usados para filtrar y ordenar antes de declarar almacenamiento. El siguiente fragmento de manifiesto sandboxed indexa ambos campos usados por la consulta:

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

El runtime puede entonces almacenar y consultar registros de trabajos:

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

Declare status y createdAt como índices en el manifiesto o en la definición de almacenamiento native antes de ejecutar esa consulta.

Endpoints REST

Mapee una ruta REST de WordPress a una ruta del plugin. EmDash la monta en /_emdash/api/plugins/<plugin-id>/<route-name>. Defina un inputSchema cuando la ruta acepte entrada y devuelva datos serializables a JSON.

Ajustes y páginas de admin

Los plugins sandboxed describen páginas de admin con Block Kit y leen o escriben valores mediante rutas y KV. No envían React a la aplicación de admin.

Los plugins native pueden usar admin.settingsSchema para un formulario generado. Use una exportación adminEntry del paquete para páginas React personalizadas, widgets, widgets de campo o columnas de listas.

Archivos y medios

Use las APIs de medios para archivos subidos o generados. Los plugins sandboxed no tienen acceso al sistema de archivos. Los plugins native comparten el proceso del host, pero escribir archivos locales del deployment no es una estrategia de almacenamiento portable.

Portar el plugin

  1. Inventarie hooks, opciones, tablas personalizadas, cron jobs, rutas REST, páginas de admin, bloques, shortcodes y hosts externos de WordPress.

  2. Elimine comportamiento que pertenezca al enrutamiento de Astro, al modelo de contenido de EmDash o a la plataforma de deployment.

  3. Elija el formato de paquete sandboxed o native. Registre cada capability y host permitido que necesite el comportamiento restante.

  4. Defina claves KV y colecciones de almacenamiento estructurado. Añada índices para cada campo usado en where u orderBy.

  5. Porte un comportamiento observable a la vez. Pruebe el hook o la ruta con contenido representativo y casos de error.

  6. Añada Block Kit o UI de admin native solo después de que funcionen las rutas y el almacenamiento subyacentes.

  7. Pruebe instalación, actualización, activación, desactivación, desinstalación con y sin borrado de datos, y cambios de capabilities.

Próximos pasos