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.
| Requisito | Sandboxed | Native |
|---|---|---|
| Instalação via registry | Sim | Não |
| Runtime isolado | Sim, com runner configurado | Não |
| Hooks, rotas, KV, armazenamento estruturado | Sim | Sim |
| Páginas de admin Block Kit | Sim | Sim |
| Componentes React de admin personalizados | Não | Sim |
| Componentes Astro para renderização pública | Não | Sim |
| Fragmentos de página brutos | Não | Sim |
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:
| WordPress | EmDash |
|---|---|
register_activation_hook() | plugin:install na primeira instalação, ou plugin:activate ao habilitar |
register_uninstall_hook() | plugin:uninstall |
wp_insert_post_data | content:beforeSave |
save_post | content:afterSave |
before_delete_post | content:beforeDelete |
deleted_post | content:afterDelete |
wp_handle_upload_prefilter | media:beforeUpload |
add_attachment | media: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:
| Capability | API disponibilizada |
|---|---|
content:read | Ler conteúdo e registrar hooks de conteúdo que expõem dados de entrada |
content:write | Criar, atualizar, publicar ou excluir conteúdo; também implica leitura |
media:read | Ler registros de mídia |
media:write | Criar ou atualizar mídia; também implica leitura |
network:request | Usar 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
-
Inventarie hooks, opções, tabelas personalizadas, cron jobs, rotas REST, páginas de admin, blocos, shortcodes e hosts externos do WordPress.
-
Remova comportamento que pertence ao roteamento Astro, ao modelo de conteúdo EmDash ou à plataforma de deployment.
-
Escolha o formato de pacote sandboxed ou native. Registre cada capability e host permitido que o comportamento restante precisa.
-
Defina chaves KV e collections de armazenamento estruturado. Adicione índices para cada campo usado em
whereouorderBy. -
Porte um comportamento observável por vez. Teste hook ou rota com conteúdo representativo e casos de falha.
-
Adicione Block Kit ou UI de admin native somente depois que rotas e armazenamento subjacentes funcionarem.
-
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
- Manifesto de plugin sandboxed para contrato de confiança e metadados do pacote.
- Capabilities para acesso a conteúdo, mídia, hosts de rede e hooks.
- Storage para KV e collections indexadas.
- Páginas de admin React para UI exclusiva de native.