Hooks

Nesta página

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. Hooks funcionam de forma idêntica em plugins nativos; plugins nativos podem adicionalmente registrar page:fragments.

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 — o PluginContext com armazenamento, KV, registro e APIs controladas por capacidades

satisfies SandboxedPlugin na exportação padrão infere event do nome do hook (o tipo de evento canônico completo) e ctx como PluginContext, então manipuladores 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 de hooks

Um hook pode ser declarado como um manipulador 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çãoTipoPadrãoDescrição
prioritynumber100Ordem de execução. Números menores executam primeiro.
timeoutnumber5000Tempo máximo de execução em milissegundos.
dependenciesstring[][]IDs de plugins que devem executar antes deste hook.
errorPolicy"abort" | "continue""abort"Se deve parar o pipeline em caso de erro.
exclusivebooleanfalseApenas um plugin pode ser o provedor ativo. Usado para email:deliver e comment:moderate.
handlerfunctionA função manipuladora 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 pela primeira vez a um site.

"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 no conteúdo do site.

content:beforeSave

Executa antes do conteúdo ser salvo. Retorne conteúdo modificado ou void para deixá-lo inalterado. Lance um erro 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, registro 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. Retorne 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>

Hooks de mídia

media:beforeUpload

Executa antes de um arquivo ser enviado. Retorne metadados de arquivo modificados ou lance um erro 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 enviado com sucesso.

Evento: { media: { id, filename, mimeType, size, url, createdAt } }Retorna: Promise<void>

Hooks de páginas públicas

Estes permitem que plugins contribuam para páginas públicas renderizadas. Templates optam por incluir os componentes <EmDashHead>, <EmDashBodyStart> e <EmDashBodyEnd> de emdash/ui.

page:metadata

Contribui metadados tipados para <head> — tags meta, propriedades OpenGraph, rels <link> permitidos e JSON-LD. Disponível para plugins sandbox e nativos. Core valida, desduplicae 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:

TipoRenderizaChave 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 qualquer chave de deduplicação. O rel de link é 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 HTML bruto, scripts ou folhas de estilo para pontos de inserção de página. Apenas plugins nativos.

Plugins sandbox não podem usar este hook porque sua saída executa como código de primeira parte no navegador do visitante, fora de qualquer limite de sandbox. Para contribuições de página seguras para sandbox, use page:metadata. Veja Plugins nativos: fragmentos de página se precisar desta superfície.

Ordem de execução de hooks

Hooks executam nesta ordem:

  1. Hooks com valores de priority menores executam primeiro.
  2. Para prioridades iguais, hooks executam na ordem de registro do plugin.
  3. Hooks com dependencies aguardam esses plugins completarem.
// 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 () => {},
}

Tratamento de erros

Quando um hook lança um erro ou expira:

  • errorPolicy: "abort" — todo o pipeline para e a operação original 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

Hooks têm padrão de 5000ms. Aumente o timeout para trabalhos mais lentos:

"content:afterSave": {
	timeout: 30000,
	handler: async (event, ctx) => {
		// Long-running operation
	},
},

Referência de hooks

HookGatilhoRetornaExclusivo
plugin:installPrimeira instalação do pluginvoidNão
plugin:activatePlugin habilitadovoidNão
plugin:deactivatePlugin desabilitadovoidNão
plugin:uninstallPlugin removidovoidNão
content:beforeSaveAntes de salvar conteúdoConteúdo modificado ou voidNão
content:afterSaveApós salvar conteúdovoidNão
content:beforeDeleteAntes de excluir conteúdofalse para cancelar, ou permitirNão
content:afterDeleteApós excluir conteúdovoidNão
content:afterPublishApós publicar conteúdovoidNão
content:afterUnpublishApós despublicar conteúdovoidNão
media:beforeUploadAntes de enviar arquivoInfo de arquivo modificada ou voidNão
media:afterUploadApós enviar arquivovoidNão
cronTarefa agendada disparavoidNão
email:beforeSendAntes de enviar emailMensagem modificada, false ou voidNão
email:deliverEntregar email via transportevoidSim
email:afterSendApós enviar emailvoidNão
comment:beforeCreateAntes de armazenar comentárioEvento modificado, false ou voidNão
comment:moderateDecidir status do comentário{ status, reason? }Sim
comment:afterCreateApós armazenar comentáriovoidNão
comment:afterModerateAdmin altera status do comentáriovoidNão
page:metadataRenderização de páginaContribuições ou nullNão
page:fragmentsRenderização de página (apenas nativo)Contribuições ou nullNão

Veja a Referência de Hooks para tipos de eventos completos e assinaturas de manipuladores.