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 no momento da definição do plugin — não há registro dinâmico em tempo de execução.
Esta página cobre plugins em sandbox. Plugins nativos usam os mesmos nomes de hook e tipos de evento, mas usam o pipeline de hooks in-process e podem além disso registrar page:fragments. A rejeição de salvamento em sandbox e o comportamento de falha do runner isolado são descritos abaixo.
Assinatura do hook
Cada manipulador 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 de ciclo de vida etc.)ctx— oPluginContextcom armazenamento, KV, logging e APIs protegidas por capabilities
Atribuir a definição a uma constante tipada como SandboxedPlugin infere event a partir do nome do hook (o tipo de evento canônico completo) e ctx como PluginContext, de modo que os manipuladores não precisam de anotações de parâmetros. Exporte essa constante como default. Para referenciar um tipo de evento pelo nome em um helper, importe-o de emdash/plugin.
Configuração do hook
Um hook pode ser declarado como um manipulador simples ou encapsulado em um objeto de configuração. Prefira a forma simples, a menos que o plugin também suporte execução in-process deliberada e precise dos metadados descritos abaixo.
Simple
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved");
},
}, Full config
hooks: {
"content:afterSave": {
priority: 100,
timeout: 5000,
handler: async (event, ctx) => {
ctx.log.info("Content saved");
},
},
}, Opções de configuração
| Option | Type | Default | Description |
|---|---|---|---|
priority | number | 100 | Ordem de execução. Números menores executam primeiro. |
timeout | number | 5000 | Tempo máximo de execução em milissegundos. |
exclusive | boolean | false | Apenas um plugin pode ser o provedor ativo. Usado para email:deliver e comment:moderate. |
handler | function | — | A função manipuladora 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 | Reason |
|---|---|---|
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 de 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 metadados de upload ou interromper o upload. |
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 do e-mail. |
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 solicitação. |
page:fragments | hooks.page-fragments:register | O hook injeta conteúdo de página first-party e é só nativo. |
Hooks de ciclo de vida, cron e page:metadata não têm capability de registro. Declare a capability listada mesmo quando um hook só lê seu evento e não chama a API ctx correspondente. A declaração dá ao operador um prompt de consentimento preciso, controla a API ctx e é necessária quando o plugin roda in-process. Capabilities e segurança explica o efeito em tempo de execução.
Hooks de ciclo de vida
Executam durante a instalação, ativação, desativação e remoção do plugin.
plugin:install
Executa uma vez quando o plugin é adicionado pela primeira vez a um site.
Este exemplo assume que o manifesto declara uma coleção 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" });
},
Event: {} — Returns: Promise<void>
plugin:activate
Executa quando o plugin é habilitado (após a instalação ou ao reabilitar).
"plugin:activate": async (_event, ctx) => {
ctx.log.info("Plugin activated");
},
Event: {} — Returns: Promise<void>
plugin:deactivate
Executa quando o plugin é desabilitado (mas não removido).
"plugin:deactivate": async (_event, ctx) => {
ctx.log.info("Plugin deactivated");
},
Event: {} — Returns: 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));
}
}
},
Event: { deleteData: boolean } — Returns: Promise<void>
Hooks de conteúdo
Executam durante 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 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 entre 1 e 500 caracteres. O EmDash identifica o plugin e mostra o motivo ao editor. Resultados de erro vazios, longos demais, malformados e desconhecidos falham o salvamento 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.",
},
};
}
if (typeof content.slug === "string") {
content.slug = content.slug.toLowerCase().replace(/\s+/g, "-");
}
return content;
},
Não coloque HTML em reason. O admin renderiza o valor como texto.
Do processo host, lance ContentSaveRejectedError (exportado de emdash) em vez disso. A API retorna SAVE_REJECTED com sua mensagem. Qualquer outra exceção de qualquer modo de execução falha o salvamento com uma resposta genérica CONTENT_HOOK_ERROR.
Event: { content, collection, isNew, id, actor } — Returns: 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). 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 }),
});
}
},
Event: { content, collection, isNew, actor } — Returns: 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 permitem.
"content:beforeDelete": async (event, ctx) => {
if (event.collection === "pages" && event.id === "home") {
ctx.log.warn("Cannot delete home page");
return false;
}
return true;
},
Event: { id, collection, permanent: false } — Returns: 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 de novo.
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}`);
},
Event: { id, collection, permanent } — Returns: 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 ação de publicação de 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, actor.role numérico e o actor.source correspondente. O EmDash aceita a origem visual-editor apenas do token de ação assinado e de curta duração embutido em um render autenticado da barra de ferramentas; solicitações API ordinárias não podem selecionar sua origem.
Eventos de publicação e agendamento expõem o rascunho efetivo em content.data e o slug preparado em content.slug. Eventos de despublicação expõem o conteúdo atualmente ao vivo que a ação removeria.
content:beforePublish
O seguinte hook exige um marcador de aprovação antes que o conteúdo possa ficar ao vivo:
"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, MCP, de plugin, de sistema e agendada. Conteúdo agendado é verificado de novo quando chega sua hora de publicação. Uma rejeição do agendador cancela o agendamento, armazena o motivo seguro para o público e lista a entrada afetada no painel em vez de tentar de novo a mesma rejeição permanente a cada tick do agendador. Um agendamento, publicação ou exclusão bem-sucedidos limpam o registro. Um administrador pode descartar um registro obsoleto quando a entrada ou o plugin de política não estiver mais disponível.
content:beforeSchedule
Executa antes de uma entrada receber uma hora de publicação. O evento também contém scheduledAt.
Não há hook content:beforeUnschedule. Um administrador sempre pode cancelar uma publicação futura.
content:beforeUnpublish
Executa antes de o conteúdo ao vivo ser removido.
content:afterPublish
Executa depois que o conteúdo é promovido de rascunho para ao vivo. Requer a capability content:read.
Event: { content, collection } — Returns: Promise<void>
content:afterUnpublish
Executa depois que o conteúdo é revertido de ao vivo para rascunho. Requer a capability content:read.
Event: { content, collection } — Returns: Promise<void>
content:afterRestore
Executa depois que conteúdo da lixeira é restaurado. Requer a capability content:read.
Event: { content, collection } — Returns: Promise<void>
content:afterSchedule
Executa depois que o conteúdo é agendado para publicação futura. Requer a capability content:read.
Event: { content, collection } — Returns: Promise<void>
content:afterUnschedule
Executa depois que conteúdo agendado é desagendado. Requer a capability content:read.
Event: { content, collection } — Returns: Promise<void>
Hooks de mídia
media:beforeUpload
Executa antes de um arquivo ser enviado. Retorne metadados de arquivo modificados ou lance 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}` };
},
Event: { file: { name, type, size } } — Returns: arquivo modificado ou void
media:afterUpload
Executa depois que um arquivo é enviado com sucesso.
Event: { media: { id, filename, mimeType, size, url, createdAt } } — Returns: Promise<void>
Hooks de páginas públicas
Estes permitem que plugins contribuam para páginas públicas renderizadas. Templates optam incluindo os componentes <EmDashHead>, <EmDashBodyStart> e <EmDashBodyEnd> de emdash/ui.
page:metadata
Contribui metadados tipados para <head> — meta tags, propriedades OpenGraph, rel de <link> na lista permitida e JSON-LD. Disponível para plugins em sandbox e nativos. O núcleo valida, deduplica e renderiza as contribuições; 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,
},
};
},
Event:
{
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;
}
}
Returns: PageMetadataContribution | PageMetadataContribution[] | null
Contribution kinds:
| Kind | Renders | Dedupe key |
|---|---|---|
meta | <meta name="..." content="..."> | key ou name |
property | <meta property="..." content="..."> | key ou property |
link | <link rel="<allowed value>" href="..."> | canonical: singleton; alternate: key ou hreflang |
jsonld | <script type="application/ld+json"> | id (se presente) |
A primeira contribuição vence para qualquer chave de deduplicação. <EmDashHead> compõe contribuições na ordem plugins → configurações do site → metadados base fornecidos pelo template, de modo que contribuições do plugin substituem tudo abaixo. Em páginas de conteúdo, os valores do painel SEO da entrada são dobrados no contexto da página antes de gerar os metadados base — eles substituem os campos do template (e são o que seu hook vê no contexto da página), enquanto contribuições do plugin ainda vencem via deduplicação first-wins. O rel de link é restrito a uma lista permitida bloqueada por segurança (canonical, alternate, author, license, nlweb, site.standard.document); href deve ser HTTP ou HTTPS.
page:fragments
Contribui HTML bruto, scripts ou folhas de estilo para pontos de inserção da página. Somente plugins nativos.
Plugins em sandbox não podem usar este hook porque sua saída roda como código first-party no navegador do visitante, fora de qualquer limite de sandbox. Para contribuições de página seguras em sandbox, use page:metadata. Veja Plugins nativos: fragmentos de página se precisar desta superfície.
Ordem de execução dos hooks
Quando um plugin no formato sandbox roda in-process, os hooks usam o pipeline de hooks compartilhado:
- Hooks com valores
prioritymenores executam primeiro. - Com prioridades iguais, os hooks executam na ordem de registro do plugin.
- Hooks com
dependenciesaguardam que esses plugins concluam.
// 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 plugins em sandbox ativos na ordem de carregamento. Mantenha os hooks independentes: não exija que um plugin em sandbox execute antes de outro.
Tratamento de erros
Falhas de hooks em sandbox dependem de quando o hook executa:
- Um erro lançado em
content:beforeSavefalha o salvamento comCONTENT_HOOK_ERROR. Retorne o envelopeSAVE_REJECTEDdocumentado quando o editor deve ver um motivo de validação específico. - Retornar
falsedecontent:beforeDeleteinterrompe a movimentação para a lixeira. Se esse hook lançar, o EmDash registra o erro e continua a exclusão. - After-hooks de conteúdo executam depois que a operação tem sucesso. 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 de origem. Use a referência de hooks para verificar um valor de retorno específico antes de confiar no comportamento de falha.
Um plugin in-process 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 em sandbox isolado.
Timeouts
O pipeline de hooks in-process tem padrão de 5.000 ms e aceita um timeout mais longo na forma de configuração completa:
"content:afterSave": {
timeout: 30000,
handler: async (event, ctx) => {
// Long-running operation
},
},
Referência de hooks
| Hook | Trigger | Return | Exclusive |
|---|---|---|---|
plugin:install | Primeira instalação do plugin | void | No |
plugin:activate | Plugin habilitado | void | No |
plugin:deactivate | Plugin desabilitado | void | No |
plugin:uninstall | Plugin removido | void | No |
content:beforeSave | Antes de salvar conteúdo | Conteúdo modificado, envelope de rejeição ou void | No |
content:afterSave | Depois de salvar conteúdo | void | No |
content:beforeDelete | Antes de mover conteúdo para a lixeira | false para cancelar, senão permitir | No |
content:afterDelete | Depois de lixeira ou exclusão permanente | void | No |
content:afterPublish | Depois de publicar conteúdo | void | No |
content:afterUnpublish | Depois de despublicar conteúdo | void | No |
content:afterRestore | Depois de restaurar conteúdo | void | No |
content:afterSchedule | Depois de agendar conteúdo | void | No |
content:afterUnschedule | Depois de desagendar conteúdo | void | No |
media:beforeUpload | Antes do upload de arquivo | Info de arquivo modificada ou void | No |
media:afterUpload | Depois do upload de arquivo | void | No |
cron | Tarefa agendada dispara | void | No |
email:beforeSend | Antes da entrega de e-mail | Mensagem modificada, false ou void | No |
email:deliver | Entregar e-mail via transporte | void | Yes |
email:afterSend | Depois da entrega de e-mail | void | No |
comment:beforeCreate | Antes de armazenar o comentário | Evento modificado, false ou void | No |
comment:moderate | Decidir o status do comentário | { status, reason? } | Yes |
comment:afterCreate | Depois de armazenar o comentário | void | No |
comment:afterModerate | Admin altera o status do comentário | void | No |
page:metadata | Render de página | Contribuições ou null | No |
page:fragments | Render de página (só nativo) | Contribuições ou null | No |
Veja a referência de hooks para tipos de evento completos e assinaturas de manipuladores.