Portar plugins do WordPress

Nesta página

Porte um plugin do WordPress separando comportamento de conteúdo, dados armazenados, rotas HTTP e interface do usuário. Em seguida escolha o formato de plugin EmDash que atende a esses requisitos.

Verificar se o plugin pertence ao EmDash

Bons candidatos possuem comportamento independente do núcleo do WordPress, como validação de conteúdo, chamadas a APIs externas, processamento em segundo plano, registros personalizados, configurações ou uma ferramenta de admin.

Não porte um plugin cujo único propósito seja implementar uma função do WordPress que Astro ou EmDash já substituem. Exemplos: cache de páginas PHP, regras de rewrite do WordPress, seleção de template do tema ou modificações em globais do núcleo.

Para um plugin que apenas define um tipo de post ou campos personalizados com pouco comportamento em runtime, crie uma collection e arquivo seed do EmDash em vez de um plugin.

Escolher sandboxed ou native

Comece com Escolher um formato de plugin. Os dois formatos compartilham nomes de hooks e as APIs PluginContext, mas seus pacotes fonte são diferentes.

RequisitoSandboxedNative
Instalação via registrySimNão
Runtime isoladoSim, com runner configuradoNão
Hooks, rotas, KV, armazenamento estruturadoSimSim
Páginas de admin Block KitSimSim
Componentes React de admin personalizadosNãoSim
Componentes Astro para renderização públicaNãoSim
Fragmentos de página brutosNãoSim

Escolha native apenas quando o port precisar de uma superfície de build ou UI exclusiva de native.

Formato de pacote sandboxed

emdash-plugin init cria o formato sandboxed atual:

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

O manifesto contém identidade, publisher, capabilities, hosts permitidos e declarações de armazenamento. A versão normalmente vem de package.json.

O manifesto a seguir declara uma collection de armazenamento indexada e a capability exigida 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 padrão um objeto plain tipado com SandboxedPlugin. Handlers de hooks sandboxed usam { handler }; handlers de rotas sandboxed recebem (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;

A rota fica disponível em /_emdash/api/plugins/read-time/recent. Um campo de armazenamento deve ser declarado como índice antes que uma consulta possa filtrar ou ordenar por ele.

Compile o pacote com emdash-plugin build; não adicione um descritor src/index.ts escrito manualmente a este formato. Leia Seu primeiro plugin sandboxed para o package.json gerado, saída de build e registro no site.

Formato de pacote native

Um pacote native exporta uma factory de descritor para astro.config.mjs e uma factory de runtime construída com definePlugin(). Entrypoints opcionais de admin e Astro são exportações separadas do pacote.

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

O entrypoint native reduzido a seguir mostra as duas peças necessárias:

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;

Handlers de hooks native podem ser funções diretamente. Handlers de rotas native recebem um único argumento de contexto combinado. Mantenha alinhadas as cópias de descritor e runtime de id, version, capabilities e entrypoints.

Leia Seu primeiro plugin native antes de adicionar páginas React de admin, renderizadores Portable Text ou fragmentos de página.

Mapear comportamento do WordPress

Hooks

Mapeie a intenção de uma action ou filter do WordPress, não apenas o nome:

WordPressEmDash
register_activation_hook()plugin:install na primeira instalação, ou plugin:activate ao 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

Eventos de hooks têm formas tipadas próprias. Consulte a referência de hooks antes de traduzir argumentos de callback do WordPress.

Hooks de conteúdo que recebem dados de entrada exigem content:read. Adicione capabilities conforme as APIs que o port chama:

CapabilityAPI disponibilizada
content:readLer conteúdo e registrar hooks de conteúdo que expõem dados de entrada
content:writeCriar, atualizar, publicar ou excluir conteúdo; também implica leitura
media:readLer registros de mídia
media:writeCriar ou atualizar mídia; também implica leitura
network:requestUsar ctx.http para hosts listados em allowedHosts

Opções e tabelas personalizadas

Use ctx.settings para configuração do usuário e ctx.kv para pequenos valores internos. Ambos os stores são isolados por plugin. Declare credenciais como campos secret em admin.settingsSchema para o EmDash criptografá-las.

Use collections ctx.storage.<collection> declaradas para registros consultáveis do plugin. A declaração de armazenamento fica em emdash-plugin.jsonc para sandboxed e em definePlugin() para native. Não abra o banco EmDash nem interpole SQL a partir do código do plugin.

A comparação a seguir porta um valor de opção sem expor globais do WordPress ao novo 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 uma tabela personalizada do WordPress, identifique os campos usados para filtrar e ordenar antes de declarar armazenamento. O fragmento de manifesto sandboxed a seguir indexa ambos os campos usados pela consulta:

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

O runtime pode então armazenar e consultar registros de jobs:

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 e createdAt como índices no manifesto ou na definição de armazenamento native antes de executar essa consulta.

Endpoints REST

Mapeie uma rota REST do WordPress para uma rota de plugin. O EmDash monta em /_emdash/api/plugins/<plugin-id>/<route-name>. Defina um inputSchema quando a rota aceitar entrada e retorne dados serializáveis em JSON.

Configurações e páginas de admin

Plugins sandboxed descrevem páginas de admin com Block Kit e leem ou escrevem valores via rotas e KV. Eles não enviam React para o aplicativo de admin.

Plugins native podem usar admin.settingsSchema para um formulário gerado. Use uma exportação adminEntry do pacote para páginas React personalizadas, widgets, widgets de campo ou colunas de lista.

Arquivos e mídia

Use as APIs de mídia para arquivos enviados ou gerados. Plugins sandboxed não têm acesso ao sistema de arquivos. Plugins native compartilham o processo host, mas gravar arquivos locais do deployment não é uma estratégia de armazenamento portável.

Portar o plugin

  1. Inventarie hooks, opções, tabelas personalizadas, cron jobs, rotas REST, páginas de admin, blocos, shortcodes e hosts externos do WordPress.

  2. Remova comportamento que pertence ao roteamento Astro, ao modelo de conteúdo EmDash ou à plataforma de deployment.

  3. Escolha o formato de pacote sandboxed ou native. Registre cada capability e host permitido que o comportamento restante precisa.

  4. Defina chaves KV e collections de armazenamento estruturado. Adicione índices para cada campo usado em where ou orderBy.

  5. Porte um comportamento observável por vez. Teste hook ou rota com conteúdo representativo e casos de falha.

  6. Adicione Block Kit ou UI de admin native somente depois que rotas e armazenamento subjacentes funcionarem.

  7. Teste instalação, upgrade, ativação, desativação, desinstalação com e sem exclusão de dados e mudanças de capabilities.

Próximos passos