Hooks permitem que plugins executem código em resposta a eventos. Todos os hooks recebem um objeto de evento e o contexto do plugin. Hooks são declarados na definição do plugin, não registrados dinamicamente em runtime.
Assinatura do hook
Todo handler de hook recebe dois argumentos:
async (event: EventType, ctx: PluginContext) => ReturnType;
event— Dados sobre o evento (conteúdo sendo salvo, mídia enviada, etc.)ctx— O contexto do plugin com storage, KV, logging e APIs condicionadas a capabilities
Configuração de hooks
Hooks podem ser declarados como um handler simples ou com configuração completa:
Simples
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved");
}
} Config 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 menores rodam primeiro. |
timeout | number | 5000 | Tempo máximo de execução em milissegundos. |
dependencies | string[] | [] | IDs de plugins que devem rodar antes deste hook. |
errorPolicy | "abort" | "continue" | "abort" | Se o pipeline para em caso de erro. |
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. |
Hooks de ciclo de vida
Hooks de ciclo de vida rodam durante instalação, ativação e desativação do plugin.
plugin:install
Roda uma vez quando o plugin é adicionado ao site pela primeira vez.
"plugin:install": async (_event, ctx) => {
ctx.log.info("Installing plugin...");
// Seed default data
await ctx.kv.set("settings:enabled", true);
await ctx.storage.items!.put("default", { name: "Default Item" });
}
Evento: {}
Retorno: Promise<void>
plugin:activate
Roda quando o plugin é habilitado (após instalar ou ao ser reabilitado).
"plugin:activate": async (_event, ctx) => {
ctx.log.info("Plugin activated");
}
Evento: {}
Retorno: Promise<void>
plugin:deactivate
Roda quando o plugin é desabilitado (mas não removido).
"plugin:deactivate": async (_event, ctx) => {
ctx.log.info("Plugin deactivated");
// Release resources, pause background work
}
Evento: {}
Retorno: Promise<void>
plugin:uninstall
Roda quando o plugin é removido do site.
"plugin:uninstall": async (event, ctx) => {
ctx.log.info("Uninstalling plugin...");
if (event.deleteData) {
// User opted to delete plugin data
const result = await ctx.storage.items!.query({ limit: 1000 });
await ctx.storage.items!.deleteMany(result.items.map(i => i.id));
}
}
Evento: { deleteData: boolean }
Retorno: Promise<void>
Hooks de conteúdo
Hooks de conteúdo rodam durante operações de criar, atualizar e excluir.
content:beforeSave
Roda antes do conteúdo ser salvo. Retorne conteúdo modificado ou void para mantê-lo inalterado. Lance erro para cancelar o save.
"content:beforeSave": async (event, ctx) => {
const { content, collection, isNew } = event;
// Validate
if (collection === "posts" && !content.title) {
throw new Error("Posts require a title");
}
// Transform
if (content.slug) {
content.slug = content.slug.toLowerCase().replace(/\s+/g, "-");
}
return content;
}
Evento:
{
content: Record<string, unknown>; // Content data being saved
collection: string; // Collection name
isNew: boolean; // True if creating, false if updating
}
Retorno: Promise<Record<string, unknown> | void>
content:afterSave
Roda após o conteúdo ser salvo com sucesso. Use para efeitos colaterais como notificações, logging ou sincronização com sistemas externos.
"content:afterSave": async (event, ctx) => {
const { content, collection, isNew } = event;
ctx.log.info(`${isNew ? "Created" : "Updated"} ${collection}/${content.id}`);
// Trigger external sync
if (ctx.http) {
await ctx.http.fetch("https://api.example.com/webhook", {
method: "POST",
body: JSON.stringify({ event: "content:save", id: content.id })
});
}
}
Evento:
{
content: Record<string, unknown>; // Saved content (includes id, timestamps)
collection: string;
isNew: boolean;
}
Retorno: Promise<void>
content:beforeDelete
Roda antes do conteúdo ser excluído. Retorne false para cancelar a exclusão, true ou void para permitir.
"content:beforeDelete": async (event, ctx) => {
const { id, collection } = event;
// Prevent deletion of protected content
if (collection === "pages" && id === "home") {
ctx.log.warn("Cannot delete home page");
return false;
}
return true;
}
Evento:
{
id: string; // Content ID being deleted
collection: string;
}
Retorno: Promise<boolean | void>
content:afterDelete
Roda após o conteúdo ser excluído com sucesso.
"content:afterDelete": async (event, ctx) => {
const { id, collection } = event;
ctx.log.info(`Deleted ${collection}/${id}`);
// Clean up related plugin data
await ctx.storage.cache!.delete(`${collection}:${id}`);
}
Evento:
{
id: string;
collection: string;
}
Retorno: Promise<void>
content:afterPublish
Roda após o conteúdo ser publicado (promovido de rascunho para ao vivo). Use para efeitos colaterais como invalidação de cache, notificações ou sincronização com sistemas externos.
Requer capability read:content.
"content:afterPublish": async (event, ctx) => {
const { content, collection } = event;
ctx.log.info(`Published ${collection}/${content.id}`);
// Notify external system
if (ctx.http) {
await ctx.http.fetch("https://api.example.com/webhook", {
method: "POST",
body: JSON.stringify({ event: "content:publish", id: content.id })
});
}
}
Evento:
{
content: Record<string, unknown>; // Published content (includes id, timestamps)
collection: string;
}
Retorno: Promise<void>
content:afterUnpublish
Roda após o conteúdo ser despublicado (revertido de ao vivo para rascunho). Use para efeitos colaterais como invalidação de cache ou notificação a sistemas externos.
Requer capability read:content.
"content:afterUnpublish": async (event, ctx) => {
const { content, collection } = event;
ctx.log.info(`Unpublished ${collection}/${content.id}`);
}
Evento:
{
content: Record<string, unknown>; // Unpublished content
collection: string;
}
Retorno: Promise<void>
Hooks de mídia
Hooks de mídia rodam durante uploads de arquivo.
media:beforeUpload
Roda antes de um arquivo ser enviado. Retorne informações de arquivo modificadas ou void para mantê-las inalteradas. Lance erro para cancelar o upload.
"media:beforeUpload": async (event, ctx) => {
const { file } = event;
// Validate file type
if (!file.type.startsWith("image/")) {
throw new Error("Only images are allowed");
}
// Validate file size (10MB max)
if (file.size > 10 * 1024 * 1024) {
throw new Error("File too large");
}
// Rename file
return {
...file,
name: `${Date.now()}-${file.name}`
};
}
Evento:
{
file: {
name: string; // Original filename
type: string; // MIME type
size: number; // Size in bytes
}
}
Retorno: Promise<{ name: string; type: string; size: number } | void>
media:afterUpload
Roda após um arquivo ser enviado com sucesso.
"media:afterUpload": async (event, ctx) => {
const { media } = event;
ctx.log.info(`Uploaded ${media.filename}`, {
id: media.id,
size: media.size,
mimeType: media.mimeType
});
}
Evento:
{
media: {
id: string;
filename: string;
mimeType: string;
size: number | null;
url: string;
createdAt: string;
}
}
Retorno: Promise<void>
Ordem de execução dos hooks
Hooks rodam nesta ordem:
- Hooks com valores de
prioritymenores rodam primeiro - Para prioridades iguais, hooks rodam na ordem de registro do plugin
- Hooks com
dependenciesaguardam a conclusão daqueles plugins
// Plugin A
"content:afterSave": {
priority: 50, // Runs first
handler: async () => {}
}
// Plugin B
"content:afterSave": {
priority: 100, // Runs second (default priority)
handler: async () => {}
}
// Plugin C
"content:afterSave": {
priority: 200,
dependencies: ["plugin-a"], // Runs after A, even if priority was lower
handler: async () => {}
}
Tratamento de erros
Quando um hook lança erro ou estoura o timeout:
errorPolicy: "abort"— Todo o pipeline para. A operação original pode falhar.errorPolicy: "continue"— O erro é registrado em log e os hooks restantes ainda rodam.
"content:afterSave": {
timeout: 5000,
errorPolicy: "continue", // Don't fail the save if this hook fails
handler: async (event, ctx) => {
// External API call that might fail
await ctx.http!.fetch("https://unreliable-api.com/notify");
}
}
Timeouts
Hooks têm timeout padrão de 5000ms (5 segundos). Aumente para operações que possam demorar mais:
"content:afterSave": {
timeout: 30000, // 30 seconds
handler: async (event, ctx) => {
// Long-running operation
}
}
Hooks de página pública
Hooks de página pública permitem que plugins contribuam para o <head> e <body> de páginas renderizadas. Templates aderem usando os componentes <EmDashHead>, <EmDashBodyStart> e <EmDashBodyEnd> de emdash/ui.
page:metadata
Contribui metadados tipados para <head> — meta tags, propriedades OpenGraph, links canonical/alternate e dados estruturados JSON-LD. Funciona em modo confiável e em sandbox.
O 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 };
}
}
Retorno: PageMetadataContribution | PageMetadataContribution[] | null
Tipos de contribuição:
| Kind | Renderiza | Chave de dedupe |
|---|---|---|
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 qualquer chave de dedupe. Hrefs de link devem ser HTTP ou HTTPS.
page:fragments
Contribui HTML bruto, scripts ou markup para pontos de inserção na página. Somente plugins confiáveis — plugins em sandbox não podem usar este hook.
"page:fragments": async (event, ctx) => {
return {
kind: "external-script",
placement: "head",
src: "https://www.googletagmanager.com/gtm.js?id=GTM-XXXXX",
async: true,
};
}
Retorno: PageFragmentContribution | PageFragmentContribution[] | null
Placements: "head", "body:start", "body:end". Templates que omitem um componente para um placement ignoram silenciosamente contribuições direcionadas a ele.
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 do save de conteúdo | Conteúdo modificado ou void | Não |
content:afterSave | Após save de conteúdo | void | Não |
content:beforeDelete | Antes de excluir conteúdo | false para cancelar, senão permitir | Não |
content:afterDelete | Após excluir conteúdo | void | Não |
content:afterPublish | Após publicar conteúdo | void | Não |
content:afterUnpublish | Após despublicar conteúdo | 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 dispara | void | Não |
email:beforeSend | Antes do envio de e-mail | Mensagem modificada, false, ou void | Não |
email:deliver | Entregar e-mail via transporte | void | Sim |
email:afterSend | Após envio de e-mail | 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 do comentário | void | Não |
page:metadata | Renderização da página | Contribuições ou null | Não |
page:fragments | Renderização da página (confiável) | Contribuições ou null | Não |
Veja a Referência de hooks para tipos de evento completos e assinaturas de handlers.