Os 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 sandboxed. Os hooks funcionam de forma idêntica em plugins nativos; plugins nativos podem adicionalmente registrar page:fragments.
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 carregada, transição de ciclo de vida, etc.)ctx— oPluginContextcom storage, KV, logging e APIs controladas por capacidades
satisfies SandboxedPlugin no export padrão infere event 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. Para referenciar um tipo de evento por nome em um helper, 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:
Simples
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved");
},
}, Configuração completa
hooks: {
"content:afterSave": {
priority: 100,
timeout: 5000,
dependencies: ["audit-log"],
errorPolicy: "continue",
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 mais baixos executam primeiro. |
timeout | number | 5000 | Tempo máximo de execução em milissegundos. |
dependencies | string[] | [] | IDs de plugins que devem executar antes deste hook. |
errorPolicy | "abort" | "continue" | "abort" | Se deve parar o pipeline em caso de erro. |
exclusive | boolean | false | Apenas um plugin pode ser o provedor ativo. Usado para email:deliver e comment:moderate. |
handler | function | — | A função handler do hook. Obrigatória. |
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 a um site pela primeira vez.
"plugin:install": async (_event, ctx) => {
ctx.log.info("Installing plugin...");
await ctx.kv.set("settings:enabled", true);
await ctx.storage.items.put("default", { name: "Default Item" });
},
Evento: {} — Retorna: Promise<void>
plugin:activate
Executa quando o plugin é habilitado (após instalação ou quando 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) {
const result = await ctx.storage.items.query({ limit: 1000 });
await ctx.storage.items.deleteMany(result.items.map((i) => i.id));
}
},
Evento: { deleteData: boolean } — Retorna: Promise<void>
Hooks de conteúdo
Executam durante operações de criação, atualização e exclusão em conteúdo do site.
content:beforeSave
Executa antes do conteúdo ser salvo. Retorna conteúdo modificado, ou void para deixá-lo inalterado. Lance uma exceção para cancelar.
"content:beforeSave": async (event, ctx) => {
const { content, collection } = event;
if (collection === "posts" && !content.title) {
throw new Error("Posts require a title");
}
if (typeof content.slug === "string") {
content.slug = content.slug.toLowerCase().replace(/\s+/g, "-");
}
return content;
},
Evento: { content, collection, isNew } — Retorna: conteúdo modificado ou void.
content:afterSave
Executa após o conteúdo ser salvo com sucesso. Use para efeitos colaterais como notificações, logging ou sincronizações externas.
"content:afterSave": async (event, ctx) => {
ctx.log.info(`${event.isNew ? "Created" : "Updated"} ${event.collection}/${event.content.id}`);
if (ctx.http) {
await ctx.http.fetch("https://api.example.com/webhook", {
method: "POST",
body: JSON.stringify({ event: "content:save", id: event.content.id }),
});
}
},
Evento: { content, collection, isNew } — Retorna: Promise<void>
content:beforeDelete
Executa antes do conteúdo ser excluído. Retorna false para cancelar; true ou void permite.
"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 } — Retorna: boolean | void
content:afterDelete
Executa após o conteúdo ser excluído com sucesso.
"content:afterDelete": async (event, ctx) => {
await ctx.storage.cache.delete(`${event.collection}:${event.id}`);
},
Evento: { id, collection } — Retorna: Promise<void>
content:afterPublish
Executa após o conteúdo ser promovido de rascunho para publicado. Requer a capacidade content:read.
Evento: { content, collection } — Retorna: Promise<void>
content:afterUnpublish
Executa após o conteúdo ser revertido de publicado para rascunho. Requer a capacidade content:read.
Evento: { content, collection } — Retorna: Promise<void>
content:afterRestore
Executa após conteúdo excluído ser restaurado. Requer a capacidade content:read.
Evento: { content, collection } — Retorna: Promise<void>
content:afterSchedule
Executa após o conteúdo ser agendado para publicação futura. Requer a capacidade content:read.
Evento: { content, collection } — Retorna: Promise<void>
content:afterUnschedule
Executa após o conteúdo agendado ser desagendado. Requer a capacidade content:read.
Evento: { content, collection } — Retorna: Promise<void>
Hooks de mídia
media:beforeUpload
Executa antes de um arquivo ser carregado. Retorna metadados de 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 após um arquivo ser carregado com sucesso.
Evento: { media: { id, filename, mimeType, size, url, createdAt } } — Retorna: Promise<void>
Hooks de página pública
Estes permitem que plugins contribuam para páginas públicas renderizadas. Os templates optam por isso incluindo os componentes <EmDashHead>, <EmDashBodyStart> e <EmDashBodyEnd> de emdash/ui.
page:metadata
Contribui metadados tipados para <head> — meta tags, propriedades OpenGraph, rels <link> permitidos e JSON-LD. Disponível tanto para plugins sandboxed quanto nativos. Core 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,
},
};
},
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 };
}
}
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="canonical|alternate" href="..."> | canonical: singleton; alternate: key ou hreflang |
jsonld | <script type="application/ld+json"> | id (se presente) |
A primeira contribuição vence para cada chave de deduplicação. Link rel é restrito a uma lista de permissão de 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 de página. Apenas plugins nativos.
Plugins sandboxed não podem usar este hook porque sua saída executa como código first-party no navegador do visitante, fora de qualquer fronteira de sandbox. Para contribuições de página seguras para sandbox, use page:metadata. Veja Plugins nativos: page fragments se precisar desta superfície.
Ordem de execução dos hooks
Os hooks executam nesta ordem:
- Hooks com valores de
prioritymais baixos executam primeiro. - Para prioridades iguais, hooks executam na ordem de registro do plugin.
- 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"], // aguarda A mesmo se sua prioridade normalmente seria posterior
handler: async () => {},
}
Tratamento de erros
Quando um hook lança uma exceção ou expira:
errorPolicy: "abort"— todo o pipeline para e a operação originadora pode falhar.errorPolicy: "continue"— o erro é registrado e os hooks restantes continuam executando.
"content:afterSave": {
timeout: 5000,
errorPolicy: "continue",
handler: async (event, ctx) => {
await ctx.http!.fetch("https://unreliable-api.com/notify");
},
},
Timeouts
Os hooks têm timeout padrão de 5000ms. Aumente o timeout para trabalhos mais lentos:
"content:afterSave": {
timeout: 30000,
handler: async (event, ctx) => {
// Operação de longa duração
},
},
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 conteúdo | Conteúdo modificado ou void | Não |
content:afterSave | Após salvar conteúdo | void | Não |
content:beforeDelete | Antes de excluir conteúdo | false para cancelar, senão permite | Não |
content:afterDelete | Após excluir conteúdo | void | Não |
content:afterPublish | Após publicar | void | Não |
content:afterUnpublish | Após despublicar | void | Não |
content:afterRestore | Após restaurar | void | Não |
content:afterSchedule | Após agendar | void | Não |
content:afterUnschedule | Após desagendar | void | Não |
media:beforeUpload | Antes do upload de arquivo | Info de arquivo modificada ou void | Não |
media:afterUpload | Após upload de arquivo | void | Não |
cron | Tarefa agendada disparada | void | Não |
email:beforeSend | Antes de enviar email | Mensagem modificada, false ou void | Não |
email:deliver | Entregar email via transporte | void | Sim |
email:afterSend | Após enviar email | void | Não |
comment:beforeCreate | Antes de armazenar comentário | Evento modificado, false ou void | Não |
comment:moderate | Decidir status do comentário | { status, reason? } | Sim |
comment:afterCreate | Após armazenar comentário | void | Não |
comment:afterModerate | Admin altera status comentário | void | Não |
page:metadata | Renderização de página | Contribuições ou null | Não |
page:fragments | Renderização (apenas nativo) | Contribuições ou null | Não |
Veja a Referência de Hooks para tipos de eventos completos e assinaturas de handlers.