Hooks permitem que plugins executem código em resposta a eventos. Todos os hooks recebem um objeto de evento e o contexto do plugin, e são declarados na definição do plugin — não há registro dinâmico em tempo de execução.
Esta página trata de plugins sandboxed. Plugins nativos usam os mesmos nomes de hook e tipos de evento, mas usam o pipeline de hooks em processo e também podem registrar page:fragments. A rejeição de salvamentos no sandbox e o comportamento de falha dos runners isolados são descritos mais abaixo.
Assinatura do hook
Todo handler de hook recebe dois argumentos:
async (event, ctx) => ReturnType;
event— dados sobre o que acabou de acontecer (conteúdo sendo salvo, mídia enviada, transição do ciclo de vida etc.)ctx— oPluginContextcom armazenamento, KV, logging e APIs controladas por capabilities
Ao atribuir a definição a uma constante do tipo SandboxedPlugin, event é inferido a partir do nome do hook (o tipo de evento canônico completo) e ctx como PluginContext, então os handlers não precisam de anotações de parâmetros. Exporte essa constante como exportação padrão. Para referenciar um tipo de evento pelo nome em uma função auxiliar, importe-o de emdash/plugin.
Configuração do hook
Um hook pode ser declarado como um handler simples ou envolvido em um objeto de configuração. Prefira a forma simples, a menos que o plugin também ofereça suporte deliberado à execução em processo e precise dos metadados descritos abaixo.
Simples
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved");
},
}, Configuração completa
hooks: {
"content:afterSave": {
priority: 100,
timeout: 5000,
handler: async (event, ctx) => {
ctx.log.info("Content saved");
},
},
}, Opções de configuração
| Opção | Tipo | Padrão | Descrição |
|---|---|---|---|
priority | number | 100 | Ordem de execução. Números menores são executados primeiro. |
timeout | number | 5000 | Tempo máximo de execução em milissegundos. |
exclusive | boolean | false | Apenas um plugin pode ser o provedor ativo. Usado em email:deliver e comment:moderate. |
handler | function | — | A função handler do hook. Obrigatória. |
Capabilities necessárias
Vários hooks expõem dados protegidos ou podem alterar uma operação. O EmDash só os registra quando o manifesto declara a capability correspondente:
| Hooks | Capability | Motivo |
|---|---|---|
content:beforeSave | content:write | O hook pode substituir o conteúdo enviado. |
content:beforePublish, content:beforeSchedule, content:beforeUnpublish | hooks.content-policy:register | Os hooks podem rejeitar mudanças no estado de publicação. |
Outros hooks content:* | content:read | Seus eventos expõem conteúdo ou identificam uma entrada. |
media:beforeUpload | media:write | O hook pode substituir os metadados do envio ou interrompê-lo. |
media:afterUpload | media:read | Seu evento expõe o item de mídia armazenado. |
email:beforeSend, email:afterSend | hooks.email-events:register | Os hooks inspecionam eventos do ciclo de vida de e-mails. |
email:deliver | hooks.email-transport:register | O hook se torna um provedor de transporte de e-mail. |
Todos os hooks comment:* | users:read | Eventos de comentário podem conter informações de contato do autor e metadados da requisição. |
page:fragments | hooks.page-fragments:register | O hook injeta conteúdo de página próprio (first-party) e é exclusivo de plugins nativos. |
Hooks de ciclo de vida, cron e page:metadata não têm capability de registro. Declare a capability listada mesmo quando um hook apenas lê seu evento e não chama a API ctx correspondente. A declaração dá ao operador um aviso de consentimento preciso, controla a API ctx e é obrigatória quando o plugin é executado em processo. Capabilities e segurança explica o efeito em tempo de execução.
Hooks de ciclo de vida
São executados durante a instalação, ativação, desativação e remoção do plugin.
plugin:install
Executa uma vez, quando o plugin é adicionado a um site pela primeira vez.
Este exemplo pressupõe que o manifesto declara uma collection de armazenamento items:
"plugin:install": async (_event, ctx) => {
ctx.log.info("Installing plugin...");
await ctx.settings.set("enabled", true);
await ctx.storage.items.put("default", { name: "Default Item" });
},
Evento: {} — Retorna: Promise<void>
plugin:activate
Executa quando o plugin é habilitado (após a instalação ou ao ser reabilitado).
"plugin:activate": async (_event, ctx) => {
ctx.log.info("Plugin activated");
},
Evento: {} — Retorna: Promise<void>
plugin:deactivate
Executa quando o plugin é desabilitado (mas não removido).
"plugin:deactivate": async (_event, ctx) => {
ctx.log.info("Plugin deactivated");
},
Evento: {} — Retorna: Promise<void>
plugin:uninstall
Executa quando o plugin é removido de um site.
"plugin:uninstall": async (event, ctx) => {
ctx.log.info("Uninstalling plugin...");
if (event.deleteData) {
while (true) {
const result = await ctx.storage.items.query({ limit: 100 });
if (result.items.length === 0) break;
await ctx.storage.items.deleteMany(result.items.map((item) => item.id));
}
}
},
Evento: { deleteData: boolean } — Retorna: Promise<void>
Hooks de conteúdo
São executados durante as operações de criação, atualização e exclusão do conteúdo do site.
content:beforeSave
Executa antes de o conteúdo ser salvo. Retorne o conteúdo modificado, um resultado de erro de hook de sandbox ou void para deixá-lo inalterado.
Para rejeitar um salvamento a partir do sandbox, retorne um resultado de hook versionado com um erro SAVE_REJECTED. Defina reason como texto simples de 1 a 500 caracteres. O EmDash identifica o plugin e mostra o motivo ao editor. Resultados de erro vazios, longos demais, malformados e desconhecidos fazem o salvamento falhar com um erro de hook genérico.
"content:beforeSave": async (event, ctx) => {
const { content } = event;
if (typeof content.title !== "string" || content.title.trim() === "") {
return {
__emdashSandboxHookResult: true,
version: 1,
error: {
code: "SAVE_REJECTED",
reason: "Add a title before saving.",
},
};
}
content.title = content.title.trim();
return content;
},
Não coloque HTML em reason. O painel de administração renderiza o valor como texto.
No processo host, lance ContentSaveRejectedError (exportado de emdash) em vez disso. A API retorna SAVE_REJECTED com a sua mensagem. Qualquer outra exceção, em qualquer um dos dois modos de execução, faz o salvamento falhar com uma resposta genérica CONTENT_HOOK_ERROR.
Evento: { content, collection, isNew, id, actor } — Retorna: conteúdo modificado, um resultado de erro de hook de sandbox ou void. Em uma atualização, id é o ID do item existente e content contém apenas os valores de campo enviados; carregue o item armazenado com ctx.content.get(event.collection, event.id). O slug da entrada não faz parte de content, e uma chave slug retornada pelo hook falha na validação por ser um campo desconhecido. Salvamentos autenticados de REST, edição visual e MCP incluem actor.id e o actor.role numérico. Escritas internas sem usuário autenticado omitem actor.
content:afterSave
Executa depois que o conteúdo é salvo com sucesso. Use para efeitos colaterais como notificações, logging ou sincronizações externas.
"content:afterSave": async (event, ctx) => {
const contentId = String(event.content.id);
ctx.log.info(`${event.isNew ? "Created" : "Updated"} ${event.collection}/${contentId}`, {
actorId: event.actor?.id,
});
if (ctx.http) {
await ctx.http.fetch("https://api.example.com/webhook", {
method: "POST",
body: JSON.stringify({ event: "content:save", id: contentId }),
});
}
},
Evento: { content, collection, isNew, actor } — Retorna: Promise<void>. Salvamentos autenticados incluem o mesmo snapshot opcional de actor que content:beforeSave.
content:beforeDelete
Executa antes de o conteúdo ser excluído. Retorne false para cancelar; true ou void permite a exclusão.
"content:beforeDelete": async (event, ctx) => {
if (event.collection === "pages" && event.id === "home") {
ctx.log.warn("Cannot delete home page");
return false;
}
return true;
},
Evento: { id, collection, permanent: false } — Retorna: boolean | void
Este hook executa antes de uma entrada ser movida para a lixeira. Remover uma entrada permanentemente da lixeira não executa content:beforeDelete novamente.
content:afterDelete
Executa depois que o conteúdo é excluído com sucesso.
"content:afterDelete": async (event, ctx) => {
await ctx.storage.cache.delete(`${event.collection}:${event.id}`);
},
Evento: { id, collection, permanent } — Retorna: Promise<void>. permanent é false quando a entrada foi movida para a lixeira e true quando foi removida permanentemente.
Declare hooks.content-policy:register para inspecionar e rejeitar publicação, agendamento ou despublicação sem receber acesso de leitura, escrita ou de ações de publicação ao conteúdo.
Retorne void para permitir a ação ou { cancel: true, reason } para rejeitá-la. O motivo deve conter de 1 a 500 caracteres de texto simples. Decisões inválidas e erros inesperados abortam por padrão, sem expor a exceção. Rejeições explícitas retornam PUBLISH_REJECTED, SCHEDULE_REJECTED ou UNPUBLISH_REJECTED.
Os três eventos contêm { content, collection, origin, actor? }. origin.source é api, mcp, visual-editor, plugin, scheduler ou system; origens de plugin também contêm pluginId. Ações humanas autenticadas incluem actor.id, o actor.role numérico e o actor.source correspondente. O EmDash aceita a origem visual-editor somente a partir do token de ação assinado e de curta duração incorporado em uma renderização autenticada da barra de ferramentas; requisições comuns à API não podem escolher sua origem.
Os eventos de publicação e agendamento expõem o rascunho efetivo em content.data e o slug preparado em content.slug. Os eventos de despublicação expõem o conteúdo atualmente no ar que a ação removeria.
content:beforePublish
O hook a seguir exige um marcador de aprovação antes de o conteúdo poder ir ao ar:
"content:beforePublish": async (event) => {
const data = event.content.data;
const approvalStatus =
typeof data === "object" && data !== null && "approval_status" in data
? data.approval_status
: undefined;
if (approvalStatus !== "approved") {
return { cancel: true, reason: "Approve this entry before publishing." };
}
},
Este hook executa antes da publicação manual, por MCP, de plugins, do sistema e agendada. O conteúdo agendado é verificado novamente quando chega o horário de publicação. A rejeição pelo agendador cancela o agendamento da entrada, armazena o motivo seguro para exibição pública e lista a entrada afetada no painel, em vez de tentar novamente a mesma rejeição permanente a cada tick do agendador. Um agendamento, publicação ou exclusão bem-sucedido limpa o registro. Um administrador pode dispensar um registro obsoleto quando a entrada ou o plugin de política não está mais disponível.
content:beforeSchedule
Executa antes de uma entrada receber um horário de publicação. O evento também contém scheduledAt.
Não existe um hook content:beforeUnschedule. Um administrador sempre pode cancelar uma publicação futura.
content:beforeUnpublish
Executa antes de o conteúdo no ar ser removido.
content:afterPublish
Executa depois que o conteúdo é promovido de rascunho para no ar. Requer a capability content:read.
Evento: { content, collection } — Retorna: Promise<void>
content:afterUnpublish
Executa depois que o conteúdo volta de no ar para rascunho. Requer a capability content:read.
Evento: { content, collection } — Retorna: Promise<void>
content:afterRestore
Executa depois que o conteúdo da lixeira é restaurado. Requer a capability content:read.
Evento: { content, collection } — Retorna: Promise<void>
content:afterSchedule
Executa depois que o conteúdo é agendado para publicação futura. Requer a capability content:read.
Evento: { content, collection } — Retorna: Promise<void>
content:afterUnschedule
Executa depois que o agendamento do conteúdo agendado é cancelado. Requer a capability content:read.
Evento: { content, collection } — Retorna: Promise<void>
Hooks de mídia
media:beforeUpload
Executa antes de um arquivo ser enviado. Retorne os metadados do arquivo modificados ou lance uma exceção para cancelar.
"media:beforeUpload": async (event, ctx) => {
if (!event.file.type.startsWith("image/")) {
throw new Error("Only images are allowed");
}
if (event.file.size > 10 * 1024 * 1024) {
throw new Error("File too large");
}
return { ...event.file, name: `${Date.now()}-${event.file.name}` };
},
Evento: { file: { name, type, size } } — Retorna: arquivo modificado ou void
media:afterUpload
Executa depois que um arquivo é enviado com sucesso.
Evento: { media: { id, filename, mimeType, size, url, createdAt } } — Retorna: Promise<void>
Hooks de páginas públicas
Permitem que plugins contribuam para as páginas públicas renderizadas. Os templates ativam esses hooks incluindo os componentes <EmDashHead>, <EmDashBodyStart> e <EmDashBodyEnd> de emdash/ui.
page:metadata
Contribui com metadados tipados para o <head> — meta tags, propriedades OpenGraph, <link> rels de uma lista de permissões e JSON-LD. Disponível tanto para plugins sandboxed quanto nativos. O core valida, deduplica e renderiza as contribuições; os plugins retornam dados estruturados, nunca HTML bruto.
"page:metadata": async (event, ctx) => {
if (event.page.kind !== "content") return null;
return {
kind: "jsonld",
id: `schema:${event.page.content?.collection}:${event.page.content?.id}`,
graph: {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: event.page.pageTitle ?? event.page.title,
description: event.page.description,
},
};
},
Evento:
{
page: {
url: string;
path: string;
locale: string | null;
kind: "content" | "custom";
pageType: string;
title: string | null;
pageTitle?: string | null;
description: string | null;
canonical: string | null;
image: string | null;
content?: { collection: string; id: string; slug: string | null };
seo?: {
ogTitle?: string | null;
ogDescription?: string | null;
ogImage?: string | null;
robots?: string | null;
};
articleMeta?: {
publishedTime?: string | null;
modifiedTime?: string | null;
author?: string | null;
};
siteName?: string;
breadcrumbs?: Array<{ name: string; url: string }>;
siteUrl?: string;
}
}
Retorna: PageMetadataContribution | PageMetadataContribution[] | null
Tipos de contribuição:
| Tipo | Renderiza | Chave de deduplicação |
|---|---|---|
meta | <meta name="..." content="..."> | key ou name |
property | <meta property="..." content="..."> | key ou property |
link | <link rel="<allowed value>" href="..."> | canonical: único; alternate: key ou hreflang |
jsonld | <script type="application/ld+json"> | id (se presente) |
Em qualquer chave de deduplicação, a primeira contribuição vence. O <EmDashHead> compõe as contribuições na ordem plugins → configurações do site → metadados base fornecidos pelo template, de modo que as contribuições dos plugins sobrepõem tudo o que está abaixo. Nas páginas de conteúdo, os valores do painel de SEO da entrada são incorporados ao contexto da página antes de os metadados base serem gerados — eles substituem os campos fornecidos pelo template (e são o que o seu hook vê no contexto da página), enquanto as contribuições dos plugins continuam vencendo pela deduplicação em que o primeiro vence. O rel dos links é restrito a uma lista de permissões bloqueada por segurança (canonical, alternate, author, license, nlweb, site.standard.document); href deve ser HTTP ou HTTPS.
page:fragments
Contribui com HTML bruto, scripts ou folhas de estilo para os pontos de inserção da página. Somente plugins nativos.
Plugins sandboxed não podem usar este hook porque sua saída é executada como código próprio (first-party) no navegador do visitante, fora de qualquer limite do sandbox. Para contribuições de página seguras para o sandbox, use page:metadata. Consulte Plugins nativos: fragmentos de página se precisar dessa superfície.
Ordem de execução dos hooks
Quando um plugin no formato sandboxed é executado em processo, os hooks usam o pipeline de hooks compartilhado:
- Hooks com valores de
prioritymenores são executados primeiro. - Em caso de prioridades iguais, os hooks são executados na ordem de registro dos plugins.
- Hooks com
dependenciesaguardam a conclusão desses plugins.
// Plugin A
"content:afterSave": { priority: 50, handler: async () => {} }
// Plugin B
"content:afterSave": { priority: 100, handler: async () => {} }
// Plugin C
"content:afterSave": {
priority: 200,
dependencies: ["plugin-a"], // waits for A even if its priority would normally be later
handler: async () => {},
}
Um runner de sandbox isolado invoca os plugins sandboxed ativos na ordem de carregamento. Mantenha os hooks independentes: não exija que um plugin sandboxed seja executado antes de outro.
Tratamento de erros
As falhas de hooks sandboxed dependem de quando o hook é executado:
- Um erro lançado em
content:beforeSavefaz o salvamento falhar comCONTENT_HOOK_ERROR. Retorne o envelopeSAVE_REJECTEDdocumentado quando o editor precisar ver um motivo de validação específico. - Retornar
falsedecontent:beforeDeleteinterrompe a movimentação para a lixeira. Se esse hook lançar uma exceção, o EmDash registra o erro e continua a exclusão. - Os hooks after de conteúdo são executados depois que a operação é bem-sucedida. Seus erros são registrados e não podem reverter a operação.
- Hooks de ciclo de vida, mídia, e-mail e comentário seguem o contrato da operação que os originou. Use a Referência de hooks para verificar um valor de retorno específico antes de depender do comportamento de falha.
Um plugin em processo pode usar errorPolicy: "abort" ou "continue" na forma de configuração completa. Essa configuração não é um controle de recuperação portátil para um plugin sandboxed isolado.
Timeouts
O pipeline de hooks em processo tem padrão de 5.000 ms e aceita um timeout maior na forma de configuração completa:
"content:afterSave": {
timeout: 30000,
handler: async (event, ctx) => {
// Long-running operation
},
},
Referência de hooks
| Hook | Gatilho | Retorno | Exclusivo |
|---|---|---|---|
plugin:install | Primeira instalação do plugin | void | Não |
plugin:activate | Plugin habilitado | void | Não |
plugin:deactivate | Plugin desabilitado | void | Não |
plugin:uninstall | Plugin removido | void | Não |
content:beforeSave | Antes de salvar o conteúdo | Conteúdo modificado, envelope de rejeição ou void | Não |
content:afterSave | Depois de salvar o conteúdo | void | Não |
content:beforeDelete | Antes de mover o conteúdo para a lixeira | false para cancelar, senão permitir | Não |
content:afterDelete | Depois de mover para a lixeira ou excluir permanentemente | void | Não |
content:afterPublish | Depois de publicar o conteúdo | void | Não |
content:afterUnpublish | Depois de despublicar o conteúdo | void | Não |
content:afterRestore | Depois de restaurar o conteúdo | void | Não |
content:afterSchedule | Depois de agendar o conteúdo | void | Não |
content:afterUnschedule | Depois de cancelar o agendamento do conteúdo | void | Não |
media:beforeUpload | Antes do envio do arquivo | Informações do arquivo modificadas ou void | Não |
media:afterUpload | Depois do envio do arquivo | void | Não |
cron | Tarefa agendada é disparada | void | Não |
email:beforeSend | Antes da entrega do e-mail | Mensagem modificada, false ou void | Não |
email:deliver | Entregar e-mail via transporte | void | Sim |
email:afterSend | Depois da entrega do e-mail | void | Não |
comment:beforeCreate | Antes de armazenar o comentário | Evento modificado, false ou void | Não |
comment:moderate | Decidir o status do comentário | { status, reason? } | Sim |
comment:afterCreate | Depois de armazenar o comentário | void | Não |
comment:afterModerate | Administrador altera o status do comentário | void | Não |
byline:afterSave | Depois de salvar a byline | void | Não |
byline:afterDelete | Depois de excluir a byline | void | Não |
page:metadata | Renderização da página | Contribuições ou null | Não |
page:fragments | Renderização da página (somente nativo) | Contribuições ou null | Não |
Consulte a Referência de hooks para ver os tipos de evento completos e as assinaturas dos handlers.