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 na definição do plugin — não há registro dinâmico em tempo de execução.

Esta página trata de plugins sandboxed. Plugins nativos usam os mesmos nomes de hook e tipos de evento, mas usam o pipeline de hooks em processo e também podem registrar page:fragments. A rejeição de salvamentos no sandbox e o comportamento de falha dos runners isolados são descritos mais abaixo.

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 enviada, transição do ciclo de vida etc.)
  • ctx — o PluginContext com armazenamento, KV, logging e APIs controladas por capabilities

Ao atribuir a definição a uma constante do tipo SandboxedPlugin, event é inferido 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. Exporte essa constante como exportação padrão. Para referenciar um tipo de evento pelo nome em uma função auxiliar, 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. Prefira a forma simples, a menos que o plugin também ofereça suporte deliberado à execução em processo e precise dos metadados descritos abaixo.

Simples

hooks: {
	"content:afterSave": async (event, ctx) => {
		ctx.log.info("Content saved");
	},
},

Configuração completa

hooks: {
	"content:afterSave": {
		priority: 100,
		timeout: 5000,
		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 são executados primeiro.
timeoutnumber5000Tempo máximo de execução em milissegundos.
exclusivebooleanfalseApenas um plugin pode ser o provedor ativo. Usado em email:deliver e comment:moderate.
handlerfunction—A função handler 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:

HooksCapabilityMotivo
content:beforeSavecontent:writeO hook pode substituir o conteúdo enviado.
content:beforePublish, content:beforeSchedule, content:beforeUnpublishhooks.content-policy:registerOs hooks podem rejeitar mudanças no estado de publicação.
Outros hooks content:*content:readSeus eventos expõem conteúdo ou identificam uma entrada.
media:beforeUploadmedia:writeO hook pode substituir os metadados do envio ou interrompê-lo.
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 de e-mails.
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 requisição.
page:fragmentshooks.page-fragments:registerO hook injeta conteúdo de página próprio (first-party) e é exclusivo de plugins nativos.

Hooks de ciclo de vida, cron e page:metadata não têm capability de registro. Declare a capability listada mesmo quando um hook apenas lê seu evento e não chama a API ctx correspondente. A declaração dá ao operador um aviso de consentimento preciso, controla a API ctx e é obrigatória quando o plugin é executado em processo. Capabilities e segurança explica o efeito em tempo de execução.

Hooks de ciclo de vida

São executados 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.

Este exemplo pressupõe que o manifesto declara uma collection 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" });
},

Evento: {} — Retorna: Promise<void>

plugin:activate

Executa quando o plugin é habilitado (após a instalação ou ao ser 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) {
		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));
		}
	}
},

Evento: { deleteData: boolean } — Retorna: Promise<void>

Hooks de conteúdo

São executados durante as 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 o 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 de 1 a 500 caracteres. O EmDash identifica o plugin e mostra o motivo ao editor. Resultados de erro vazios, longos demais, malformados e desconhecidos fazem o salvamento falhar 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.",
			},
		};
	}

	content.title = content.title.trim();

	return content;
},

Não coloque HTML em reason. O painel de administração renderiza o valor como texto.

No processo host, lance ContentSaveRejectedError (exportado de emdash) em vez disso. A API retorna SAVE_REJECTED com a sua mensagem. Qualquer outra exceção, em qualquer um dos dois modos de execução, faz o salvamento falhar com uma resposta genérica CONTENT_HOOK_ERROR.

Evento: { content, collection, isNew, id, actor } — Retorna: 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). O slug da entrada não faz parte de content, e uma chave slug retornada pelo hook falha na validação por ser um campo desconhecido. 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 }),
		});
	}
},

Evento: { content, collection, isNew, actor } — Retorna: 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 permite a exclusão.

"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, permanent: false } — Retorna: 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 novamente.

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}`);
},

Evento: { id, collection, permanent } — Retorna: 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 de ações de publicação ao 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, o actor.role numérico e o actor.source correspondente. O EmDash aceita a origem visual-editor somente a partir do token de ação assinado e de curta duração incorporado em uma renderização autenticada da barra de ferramentas; requisições comuns à API não podem escolher sua origem.

Os eventos de publicação e agendamento expõem o rascunho efetivo em content.data e o slug preparado em content.slug. Os eventos de despublicação expõem o conteúdo atualmente no ar que a ação removeria.

content:beforePublish

O hook a seguir exige um marcador de aprovação antes de o conteúdo poder ir ao ar:

"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, por MCP, de plugins, do sistema e agendada. O conteúdo agendado é verificado novamente quando chega o horário de publicação. A rejeição pelo agendador cancela o agendamento da entrada, armazena o motivo seguro para exibição pública e lista a entrada afetada no painel, em vez de tentar novamente a mesma rejeição permanente a cada tick do agendador. Um agendamento, publicação ou exclusão bem-sucedido limpa o registro. Um administrador pode dispensar um registro obsoleto quando a entrada ou o plugin de política não está mais disponível.

content:beforeSchedule

Executa antes de uma entrada receber um horário de publicação. O evento também contém scheduledAt.

Não existe um hook content:beforeUnschedule. Um administrador sempre pode cancelar uma publicação futura.

content:beforeUnpublish

Executa antes de o conteúdo no ar ser removido.

content:afterPublish

Executa depois que o conteúdo é promovido de rascunho para no ar. Requer a capability content:read.

Evento: { content, collection } — Retorna: Promise<void>

content:afterUnpublish

Executa depois que o conteúdo volta de no ar para rascunho. Requer a capability content:read.

Evento: { content, collection } — Retorna: Promise<void>

content:afterRestore

Executa depois que o conteúdo da lixeira é restaurado. Requer a capability content:read.

Evento: { content, collection } — Retorna: Promise<void>

content:afterSchedule

Executa depois que o conteúdo é agendado para publicação futura. Requer a capability content:read.

Evento: { content, collection } — Retorna: Promise<void>

content:afterUnschedule

Executa depois que o agendamento do conteúdo agendado é cancelado. Requer a capability content:read.

Evento: { content, collection } — Retorna: Promise<void>

Hooks de mídia

media:beforeUpload

Executa antes de um arquivo ser enviado. Retorne os metadados do 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 depois que um arquivo é enviado com sucesso.

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

Hooks de páginas públicas

Permitem que plugins contribuam para as páginas públicas renderizadas. Os templates ativam esses hooks incluindo os componentes <EmDashHead>, <EmDashBodyStart> e <EmDashBodyEnd> de emdash/ui.

page:metadata

Contribui com metadados tipados para o <head> — meta tags, propriedades OpenGraph, <link> rels de uma lista de permissões e JSON-LD. Disponível tanto para plugins sandboxed quanto nativos. O core valida, deduplica e renderiza as contribuições; os 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 };
		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;
	}
}

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="<allowed value>" href="...">canonical: único; alternate: key ou hreflang
jsonld<script type="application/ld+json">id (se presente)

Em qualquer chave de deduplicação, a primeira contribuição vence. O <EmDashHead> compõe as contribuições na ordem plugins → configurações do site → metadados base fornecidos pelo template, de modo que as contribuições dos plugins sobrepõem tudo o que está abaixo. Nas páginas de conteúdo, os valores do painel de SEO da entrada são incorporados ao contexto da página antes de os metadados base serem gerados — eles substituem os campos fornecidos pelo template (e são o que o seu hook vê no contexto da página), enquanto as contribuições dos plugins continuam vencendo pela deduplicação em que o primeiro vence. O rel dos links é 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 com HTML bruto, scripts ou folhas de estilo para os pontos de inserção da página. Somente plugins nativos.

Plugins sandboxed não podem usar este hook porque sua saída é executada como código próprio (first-party) no navegador do visitante, fora de qualquer limite do sandbox. Para contribuições de página seguras para o sandbox, use page:metadata. Consulte Plugins nativos: fragmentos de página se precisar dessa superfície.

Ordem de execução dos hooks

Quando um plugin no formato sandboxed é executado em processo, os hooks usam o pipeline de hooks compartilhado:

  1. Hooks com valores de priority menores são executados primeiro.
  2. Em caso de prioridades iguais, os hooks são executados na ordem de registro dos plugins.
  3. Hooks com dependencies aguardam 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"],   // waits for A even if its priority would normally be later
	handler: async () => {},
}

Um runner de sandbox isolado invoca os plugins sandboxed ativos na ordem de carregamento. Mantenha os hooks independentes: não exija que um plugin sandboxed seja executado antes de outro.

Tratamento de erros

As falhas de hooks sandboxed dependem de quando o hook é executado:

  • Um erro lançado em content:beforeSave faz o salvamento falhar com CONTENT_HOOK_ERROR. Retorne o envelope SAVE_REJECTED documentado quando o editor precisar 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 uma exceção, o EmDash registra o erro e continua a exclusão.
  • Os hooks after de conteúdo são executados depois que a operação é bem-sucedida. 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 que os originou. Use a Referência de hooks para verificar um valor de retorno específico antes de depender do comportamento de falha.

Um plugin em processo 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 sandboxed isolado.

Timeouts

O pipeline de hooks em processo tem padrão de 5.000 ms e aceita um timeout maior na forma de configuração completa:

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

Referência de hooks

HookGatilhoRetornoExclusivo
plugin:installPrimeira instalação do pluginvoidNão
plugin:activatePlugin habilitadovoidNão
plugin:deactivatePlugin desabilitadovoidNão
plugin:uninstallPlugin removidovoidNão
content:beforeSaveAntes de salvar o conteúdoConteúdo modificado, envelope de rejeição ou voidNão
content:afterSaveDepois de salvar o conteúdovoidNão
content:beforeDeleteAntes de mover o conteúdo para a lixeirafalse para cancelar, senão permitirNão
content:afterDeleteDepois de mover para a lixeira ou excluir permanentementevoidNão
content:afterPublishDepois de publicar o conteúdovoidNão
content:afterUnpublishDepois de despublicar o conteúdovoidNão
content:afterRestoreDepois de restaurar o conteúdovoidNão
content:afterScheduleDepois de agendar o conteúdovoidNão
content:afterUnscheduleDepois de cancelar o agendamento do conteúdovoidNão
media:beforeUploadAntes do envio do arquivoInformações do arquivo modificadas ou voidNão
media:afterUploadDepois do envio do arquivovoidNão
cronTarefa agendada é disparadavoidNão
email:beforeSendAntes da entrega do e-mailMensagem modificada, false ou voidNão
email:deliverEntregar e-mail via transportevoidSim
email:afterSendDepois da entrega do e-mailvoidNão
comment:beforeCreateAntes de armazenar o comentárioEvento modificado, false ou voidNão
comment:moderateDecidir o status do comentário{ status, reason? }Sim
comment:afterCreateDepois de armazenar o comentáriovoidNão
comment:afterModerateAdministrador altera o status do comentáriovoidNão
byline:afterSaveDepois de salvar a bylinevoidNão
byline:afterDeleteDepois de excluir a bylinevoidNão
page:metadataRenderização da páginaContribuições ou nullNão
page:fragmentsRenderização da página (somente nativo)Contribuições ou nullNão

Consulte a Referência de hooks para ver os tipos de evento completos e as assinaturas dos handlers.