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. 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 — o PluginContext com 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

OptionTypeDefaultDescription
prioritynumber100Ordem de execução. Números menores executam primeiro.
timeoutnumber5000Tempo máximo de execução em milissegundos.
exclusivebooleanfalseApenas um plugin pode ser o provedor ativo. Usado para email:deliver e comment:moderate.
handlerfunction—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:

HooksCapabilityReason
content:beforeSavecontent:writeO hook pode substituir o conteúdo enviado.
content:beforePublish, content:beforeSchedule, content:beforeUnpublishhooks.content-policy:registerOs hooks podem rejeitar mudanças de estado de publicação.
Outros hooks content:*content:readSeus eventos expõem conteúdo ou identificam uma entrada.
media:beforeUploadmedia:writeO hook pode substituir metadados de upload ou interromper o upload.
media:afterUploadmedia:readSeu evento expõe o item de mídia armazenado.
email:beforeSend, email:afterSendhooks.email-events:registerOs hooks inspecionam eventos do ciclo de vida do e-mail.
email:deliverhooks.email-transport:registerO hook se torna um provedor de transporte de e-mail.
Todos os hooks comment:*users:readEventos de comentário podem conter informações de contato do autor e metadados da solicitação.
page:fragmentshooks.page-fragments:registerO 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:

KindRendersDedupe 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:

  1. Hooks com valores priority menores executam primeiro.
  2. Com prioridades iguais, os hooks executam na ordem de registro do plugin.
  3. Hooks com dependencies aguardam 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:beforeSave falha o salvamento com CONTENT_HOOK_ERROR. Retorne o envelope SAVE_REJECTED documentado quando o editor deve ver um motivo de validação específico.
  • Retornar false de content:beforeDelete interrompe 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

HookTriggerReturnExclusive
plugin:installPrimeira instalação do pluginvoidNo
plugin:activatePlugin habilitadovoidNo
plugin:deactivatePlugin desabilitadovoidNo
plugin:uninstallPlugin removidovoidNo
content:beforeSaveAntes de salvar conteúdoConteúdo modificado, envelope de rejeição ou voidNo
content:afterSaveDepois de salvar conteúdovoidNo
content:beforeDeleteAntes de mover conteúdo para a lixeirafalse para cancelar, senão permitirNo
content:afterDeleteDepois de lixeira ou exclusão permanentevoidNo
content:afterPublishDepois de publicar conteúdovoidNo
content:afterUnpublishDepois de despublicar conteúdovoidNo
content:afterRestoreDepois de restaurar conteúdovoidNo
content:afterScheduleDepois de agendar conteúdovoidNo
content:afterUnscheduleDepois de desagendar conteúdovoidNo
media:beforeUploadAntes do upload de arquivoInfo de arquivo modificada ou voidNo
media:afterUploadDepois do upload de arquivovoidNo
cronTarefa agendada disparavoidNo
email:beforeSendAntes da entrega de e-mailMensagem modificada, false ou voidNo
email:deliverEntregar e-mail via transportevoidYes
email:afterSendDepois da entrega de e-mailvoidNo
comment:beforeCreateAntes de armazenar o comentárioEvento modificado, false ou voidNo
comment:moderateDecidir o status do comentário{ status, reason? }Yes
comment:afterCreateDepois de armazenar o comentáriovoidNo
comment:afterModerateAdmin altera o status do comentáriovoidNo
page:metadataRender de páginaContribuições ou nullNo
page:fragmentsRender de página (só nativo)Contribuições ou nullNo

Veja a referência de hooks para tipos de evento completos e assinaturas de manipuladores.