Referência de hooks

Nesta página

Os hooks permitem que os plugins interceptem e modifiquem o comportamento do EmDash em pontos específicos do ciclo de vida de conteúdo, mídia, e-mail, comentários e páginas.

Visão geral dos hooks

A tabela a seguir lista cada hook, o que o dispara, o que ele pode modificar e se é exclusivo:

HookGatilhoPode modificarExclusivo
content:beforeSaveAntes de o conteúdo ser salvoDados do conteúdoNão
content:afterSaveDepois de o conteúdo ser salvoNadaNão
content:beforeDeleteAntes de o conteúdo ser excluídoPode cancelarNão
content:afterDeleteDepois de o conteúdo ser excluídoNadaNão
content:beforePublishAntes de o conteúdo ser publicadoPode cancelarNão
content:beforeScheduleAntes de o conteúdo ser agendadoPode cancelarNão
content:beforeUnpublishAntes de o conteúdo ser despublicadoPode cancelarNão
content:afterPublishDepois de o conteúdo ser publicadoNadaNão
content:afterUnpublishDepois de o conteúdo ser despublicadoNadaNão
content:afterRestoreDepois de o conteúdo ser restauradoNadaNão
content:afterScheduleDepois de o conteúdo ser agendadoNadaNão
content:afterUnscheduleDepois de o agendamento ser canceladoNadaNão
media:beforeUploadAntes de um arquivo ser enviadoMetadados do arquivoNão
media:afterUploadDepois de um arquivo ser enviadoNadaNão
cronUma tarefa agendada é disparadaNadaNão
email:beforeSendAntes da entrega do e-mailMensagem, pode cancelarNão
email:deliverEntregar o e-mail por meio do transporteNadaSim
email:afterSendDepois de uma entrega de e-mail bem-sucedidaNadaNão
comment:beforeCreateAntes de o comentário ser armazenadoComentário, pode cancelarNão
comment:moderateDecidir o status de aprovação do comentárioStatusSim
comment:afterCreateDepois de o comentário ser armazenadoNadaNão
comment:afterModerateDepois de um admin alterar o status do comentárioNadaNão
byline:afterSaveDepois de um byline ser criado ou atualizadoNadaNão
byline:afterDeleteDepois de um byline ser excluídoNadaNão
page:metadataRenderização do head da página públicaContribuir com tagsNão
page:fragmentsRenderização do body da página públicaInjetar scriptsNão
plugin:installQuando o plugin é instalado pela primeira vezNadaNão
plugin:activateQuando o plugin é ativadoNadaNão
plugin:deactivateQuando o plugin é desativadoNadaNão
plugin:uninstallQuando o plugin é removidoNadaNão

Hooks de conteúdo

content:beforeSave

Capability: content:write

É executado antes de o conteúdo ser salvo no banco de dados. Use-o para validar, transformar ou enriquecer o conteúdo. Um hook em sandbox rejeita o salvamento retornando um resultado de hook versão 1 com um erro SAVE_REJECTED e um reason em texto simples de 1–500 caracteres. A API responde com SAVE_REJECTED, e o painel de administração identifica o plugin e exibe o motivo como texto. Resultados de erro vazios, longos demais, malformados ou desconhecidos fazem o salvamento falhar com uma resposta genérica CONTENT_HOOK_ERROR.

A partir do processo host, lance ContentSaveRejectedError (exportado de emdash) para rejeitar um salvamento. Qualquer outra exceção, em qualquer um dos modos de execução, faz o salvamento falhar com uma resposta genérica que não expõe a mensagem da exceção.

import { definePlugin } from "emdash";

export default definePlugin({
	id: "my-plugin",
	version: "1.0.0",
	hooks: {
		"content:beforeSave": async (event, ctx) => {
			const { content, collection, isNew } = event;

			// Add timestamps
			if (isNew) {
				content.createdBy = "system";
			}
			content.modifiedAt = new Date().toISOString();

			// Return modified content
			return content;
		},
	},
});

Evento

interface ActorInfo {
	readonly id: string;
	readonly role: number;
}

interface ContentHookEvent {
	content: Record<string, unknown>; // Content data
	collection: string; // Collection slug
	isNew: boolean; // True for creates, false for updates
	id?: string; // ID of the existing item on updates; absent on creates
	actor?: ActorInfo; // Authenticated user that initiated the save
	locale?: string; // Locale of the entry being saved
	translationOf?: string; // Source entry ID when a create adds a translation
}

Em uma atualização, content contém apenas os valores de campo enviados. Carregue o item armazenado com ctx.content.get(event.collection, event.id) quando o hook precisar compará-lo. Salvamentos autenticados via REST, edição visual e MCP incluem actor. Gravações internas sem usuário autenticado o omitem.

locale é o locale em que a entrada é salva: em uma criação, o locale solicitado resolvido em relação aos locales configurados, ou o locale padrão quando a requisição não indica nenhum; em uma atualização, o locale da entrada armazenada. translationOf só é definido quando uma criação vem do fluxo de tradução e contém o ID da entrada que está sendo traduzida. Use os dois juntos para distinguir uma entrada nova de uma nova tradução de uma entrada existente, por exemplo para permitir apenas uma entrada por locale em uma collection:

"content:beforeSave": async (event, ctx) => {
	if (event.collection !== "home" || !event.isNew || event.translationOf) return;
	const existing = await ctx.content.list("home", {
		limit: 1,
		where: { locale: event.locale },
	});
	if (existing.items.length > 0) {
		throw new ContentSaveRejectedError("Home already exists in this locale. Add a translation instead.");
	}
},

Valor de retorno

  • Retorne o objeto de conteúdo modificado para aplicar as alterações
  • Retorne um envelope de erro de hook em sandbox para rejeitar o salvamento com um motivo em texto simples de tamanho limitado
  • Retorne void para deixar passar sem alterações

Um hook em sandbox retorna este envelope completo para rejeitar um salvamento:

return {
	__emdashSandboxHookResult: true,
	version: 1,
	error: {
		code: "SAVE_REJECTED",
		reason: "Add a summary before saving.",
	},
};

O host aplica trim em reason e aceita de 1 a 500 caracteres.

content:afterSave

Capability: content:read

É executado depois de o conteúdo ser salvo. Use-o para efeitos colaterais como notificações, invalidação de cache ou sincronização externa.

hooks: {
  "content:afterSave": async (event, ctx) => {
    const { content, collection, isNew } = event;

    if (collection === "posts" && content.status === "published") {
      // Notify external service
      await ctx.http?.fetch("https://api.example.com/notify", {
        method: "POST",
        body: JSON.stringify({ postId: content.id }),
      });
    }
  },
}

Evento

content:afterSave recebe content, collection, isNew, o actor autenticado opcional e os mesmos locale e translationOf de content:beforeSave. content é a entrada salva completa, com o ID do banco de dados em content.id e os campos da collection em content.data. O id opcional separado usado nas atualizações de content:beforeSave não está presente após o salvamento.

Valor de retorno

Nenhum valor de retorno é esperado.

content:beforeDelete

Capability: content:read

É executado antes de o conteúdo ser excluído. Use-o para validar a exclusão ou impedi-la.

hooks: {
  "content:beforeDelete": async (event, ctx) => {
    const { id, collection } = event;

    // Prevent deletion of protected content
    const item = await ctx.content?.get(collection, id);
    if (item?.data.protected) {
      return false; // Cancel deletion
    }

    // Allow deletion
    return true;
  },
}

Evento

interface ContentDeleteEvent {
	id: string; // Entry ID
	collection: string; // Collection slug
	permanent?: false; // Present for native plugins; omitted in the sandbox runtime
}

content:beforeDelete só é executado quando uma entrada é movida para a lixeira. Plugins nativos recebem permanent: false; o runtime em sandbox envia apenas id e collection. Não use esse campo para ramificar dentro de um hook em sandbox. A exclusão permanente ignora content:beforeDelete, então o hook não pode impedir que um administrador exclua definitivamente uma entrada que já está na lixeira.

Valor de retorno

  • Retorne false para cancelar a exclusão
  • Retorne true ou void para permitir

content:afterDelete

Capability: content:read

É executado depois de o conteúdo ser excluído. Use-o para tarefas de limpeza.

hooks: {
  "content:afterDelete": async (event, ctx) => {
    const { id, collection, permanent } = event;

    if (permanent) {
      await ctx.storage.relatedItems.delete(`${collection}:${id}`);
    }
  },
}

O evento contém id, collection e permanent. permanent é false quando a entrada vai para a lixeira e true quando é excluída permanentemente. Verifique-o antes de remover dados de que uma entrada restaurada da lixeira ainda precisaria. Este hook não tem valor de retorno.

content:beforePublish

Capability: hooks.content-policy:register

É executado antes de o conteúdo ser publicado. Retorne void para permitir a publicação ou { cancel: true, reason } para rejeitá-la. O EmDash aceita 1–500 caracteres em texto simples em reason e retorna PUBLISH_REJECTED em caso de cancelamento explícito.

O evento contém o content, collection e origin atuais. Ações autenticadas de API, MCP e editor visual também incluem actor com o source correspondente. A origem visual-editor exige o token de ação assinado e de curta duração de uma renderização autenticada da barra de ferramentas. Entradas agendadas executam este hook novamente quando chegam ao vencimento. Uma rejeição do agendador cancela o agendamento e lista no painel a entrada afetada e o motivo. Um agendamento, publicação ou exclusão bem-sucedido limpa o registro; um administrador pode dispensar um registro obsoleto.

content:beforeSchedule

Capability: hooks.content-policy:register

É executado antes de uma entrada receber um horário de publicação. Usa o mesmo formato de decisão e os mesmos campos de origem de content:beforePublish, adiciona scheduledAt ao evento e retorna SCHEDULE_REJECTED em caso de cancelamento explícito.

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

content:beforeUnpublish

Capability: hooks.content-policy:register

É executado antes de o conteúdo publicado ser removido do ar. Usa o mesmo formato de decisão e os mesmos campos de origem de content:beforePublish e retorna UNPUBLISH_REJECTED em caso de cancelamento explícito.

hooks.content-policy:register não implica content:read, content:write nem ações de publicação. Decisões inválidas e erros inesperados da política abort falham de forma fechada, sem expor mensagens de exceção.

content:afterPublish

É executado depois de uma entrada ser publicada com sucesso, inclusive uma entrada que o EmDash publica automaticamente no horário agendado. Use-o para trabalho que depende da entrada publicada, como notificar outro serviço ou atualizar um índice de busca externo.

hooks: {
  "content:afterPublish": async (event, ctx) => {
    ctx.log.info(`Published ${event.collection}/${event.content.id}`);
  },
}

O hook exige a capability content:read. O EmDash o executa depois da resposta de publicação, portanto seu valor de retorno não pode alterar a entrada nem desfazer a publicação. Um erro é registrado em log; com errorPolicy: "abort", os hooks de publicação seguintes não são executados.

content:afterUnpublish

É executado depois de uma entrada ser despublicada com sucesso. Use-o para remover ou atualizar cópias do conteúdo mantidas em sistemas externos. Despublicar também cancela qualquer agendamento pendente, sem executar content:afterUnschedule.

hooks: {
  "content:afterUnpublish": async (event, ctx) => {
    ctx.log.info(`Unpublished ${event.collection}/${event.content.id}`);
  },
}

Este hook tem o mesmo requisito de capability content:read, a mesma execução adiada, o mesmo formato de evento e o mesmo contrato sem valor de retorno de content:afterPublish.

content:afterRestore

É executado depois de o conteúdo da lixeira ser restaurado. Exige a capability content:read. A entrada restaurada é um rascunho sem agendamento, qualquer que fosse o seu status quando foi para a lixeira. Remover o agendamento não executa também content:afterUnschedule.

hooks: {
  "content:afterRestore": async (event, ctx) => {
    ctx.log.info(`Restored ${event.collection}/${event.content.id}`);
  },
}

content:afterSchedule

É executado depois de o conteúdo ser agendado para publicação futura. Exige a capability content:read.

hooks: {
  "content:afterSchedule": async (event, ctx) => {
    ctx.log.info(`Scheduled ${event.collection}/${event.content.id}`);
  },
}

content:afterUnschedule

É executado depois de o agendamento de um conteúdo ser cancelado. Exige a capability content:read.

hooks: {
  "content:afterUnschedule": async (event, ctx) => {
    ctx.log.info(`Unscheduled ${event.collection}/${event.content.id}`);
  },
}

Evento

interface ContentStateChangeEvent {
	content: Record<string, unknown>;
	collection: string;
}

Este formato de evento é compartilhado por content:afterPublish, content:afterUnpublish, content:afterRestore, content:afterSchedule e content:afterUnschedule. content é a entrada completa após a mudança de estado, incluindo id, slug e status; os campos da collection ficam em content.data.

Valor de retorno

Nenhum valor de retorno é esperado.

Hooks de mídia

media:beforeUpload

Capability: media:write

É executado antes de um arquivo ser enviado. Use-o para validar, renomear ou rejeitar arquivos.

hooks: {
  "media:beforeUpload": async (event, ctx) => {
    const { file } = event;

    // Reject files over 10MB
    if (file.size > 10 * 1024 * 1024) {
      throw new Error("File too large");
    }

    // Rename file
    return {
      name: `${Date.now()}-${file.name}`,
      type: file.type,
      size: file.size,
    };
  },
}

Evento

interface MediaUploadEvent {
	file: {
		name: string; // Original filename
		type: string; // MIME type
		size: number; // Size in bytes
	};
}

Valor de retorno

  • Retorne os metadados do arquivo modificados para aplicar as alterações
  • Retorne void para deixar passar sem alterações
  • Lance uma exceção para rejeitar o envio

media:afterUpload

Capability: media:read

É executado depois de um arquivo ser enviado. Use-o para processamento, miniaturas ou extração de metadados.

hooks: {
  "media:afterUpload": async (event, ctx) => {
    const { media } = event;

    if (media.mimeType.startsWith("image/")) {
      // Store image metadata
      await ctx.kv.set(`media:${media.id}:analyzed`, {
        processedAt: new Date().toISOString(),
      });
    }
  },
}

Evento

interface MediaAfterUploadEvent {
	media: {
		id: string;
		filename: string;
		mimeType: string;
		size: number | null;
		url: string;
		createdAt: string;
	};
}

Valor de retorno

Nenhum valor de retorno é esperado.

Hooks de ciclo de vida

Os hooks de ciclo de vida não exigem nenhuma capability de registro.

plugin:install

É executado quando um plugin é instalado pela primeira vez. Use-o para a configuração inicial, para criar collections de armazenamento ou dados seed.

hooks: {
  "plugin:install": async (event, ctx) => {
    // Initialize default settings
    await ctx.settings.set("enabled", true);
    await ctx.settings.set("threshold", 100);

    ctx.log.info("Plugin installed successfully");
  },
}

plugin:activate

É executado quando um plugin é ativado (após a instalação ou uma reativação).

hooks: {
  "plugin:activate": async (event, ctx) => {
    ctx.log.info("Plugin activated");
  },
}

plugin:deactivate

É executado quando um plugin é desativado.

hooks: {
  "plugin:deactivate": async (event, ctx) => {
    ctx.log.info("Plugin deactivated");
  },
}

plugin:install, plugin:activate e plugin:deactivate recebem um objeto de evento vazio. Não têm valor de retorno.

plugin:uninstall

É executado quando um plugin é removido. Use-o para limpeza.

hooks: {
  "plugin:uninstall": async (event, ctx) => {
    const { deleteData } = event;

    if (deleteData) {
      // Clean up all plugin data
      const items = await ctx.settings.list();
      for (const { key } of items) {
        await ctx.kv.delete(key);
      }
    }

    ctx.log.info("Plugin uninstalled");
  },
}

Evento

interface UninstallEvent {
	deleteData: boolean; // User chose to delete data
}

O hook de desinstalação não tem valor de retorno.

Hook cron

cron

Capability: Nenhuma necessária

É disparado quando uma tarefa agendada é executada. Agende tarefas com ctx.cron.schedule(). As expressões cron são avaliadas em UTC em cada host.

hooks: {
  "cron": async (event, ctx) => {
    if (event.name === "daily-sync") {
      const data = await ctx.http?.fetch("https://api.example.com/data");
      ctx.log.info("Sync complete");
    }
  },
}

Evento

interface CronEvent {
	name: string;
	data?: Record<string, unknown>;
	scheduledAt: string;
}

O hook cron não tem valor de retorno.

Hooks de e-mail

Para mensagens enviadas por plugins, os hooks de e-mail são executados nesta ordem: email:beforeSend, depois email:deliver, depois email:afterSend. As mensagens de autenticação do sistema vão direto para email:deliver; elas não passam por email:beforeSend nem por email:afterSend.

email:beforeSend

Capability: hooks.email-events:register

Hook middleware executado antes da entrega. Transforme mensagens ou cancele a entrega.

hooks: {
  "email:beforeSend": async (event, ctx) => {
    // Add footer to all emails
    return {
      ...event.message,
      text: event.message.text + "\n\n—Sent from My Site",
    };

    // Or return false to cancel delivery
  },
}

Evento

interface EmailBeforeSendEvent {
	message: {
		to: string;
		cc?: string[];
		replyTo?: string;
		subject: string;
		text: string;
		html?: string;
	};
	source: string;
}

Valor de retorno

  • Retorne a mensagem modificada para transformá-la
  • Retorne false para cancelar a entrega

email:deliver

Capability: hooks.email-transport:register | Exclusivo: Sim

O provedor de transporte. Apenas um plugin pode entregar e-mails. É responsável por enviar de fato a mensagem por meio de um serviço de e-mail.

hooks: {
  "email:deliver": {
    exclusive: true,
    handler: async (event, ctx) => {
      await sendViaSES(event.message);
    },
  },
}

Evento e valor de retorno

interface EmailDeliverEvent {
	message: {
		to: string;
		cc?: string[];
		replyTo?: string;
		subject: string;
		text: string;
		html?: string;
	};
	source: string;
}

O hook não tem valor de retorno. source é "system" para mensagens de autenticação do EmDash e o ID do plugin para mensagens enviadas por um plugin.

Os plugins podem definir cc (destinatários adicionais) e replyTo em uma mensagem. Os provedores devem entregar ambos quando presentes; um provedor que descarta cc silenciosamente deixa esses destinatários sem a mensagem.

email:afterSend

Capability: hooks.email-events:register

Hook fire-and-forget executado após uma entrega bem-sucedida. Os erros são registrados em log, mas não se propagam.

hooks: {
  "email:afterSend": async (event, ctx) => {
    await ctx.kv.set(`email:log:${Date.now()}`, {
      to: event.message.to,
      subject: event.message.subject,
    });
  },
}

Evento e valor de retorno

email:afterSend recebe os mesmos campos message e source de email:deliver. Não tem valor de retorno.

Hooks de comentários

Os hooks de comentários são executados nesta ordem: comment:beforeCreate, depois comment:moderate, depois comment:afterCreate. O hook comment:afterModerate é executado separadamente quando um administrador ou um plugin autorizado altera o status de um comentário.

Depois que um comentário é armazenado, os hooks recebem este formato de registro:

interface StoredComment {
	id: string;
	collection: string;
	contentId: string;
	parentId: string | null;
	authorName: string;
	authorEmail: string;
	authorUserId: string | null;
	body: string;
	status: string;
	moderationMetadata: Record<string, unknown> | null;
	createdAt: string;
	updatedAt: string;
}

comment:beforeCreate

Capability: users:read

Hook middleware executado antes de um comentário ser armazenado. Enriqueça, valide ou rejeite comentários.

hooks: {
  "comment:beforeCreate": async (event, ctx) => {
    // Reject comments with links
    if (event.comment.body.includes("http")) {
      return false;
    }
  },
}

Evento

interface CommentBeforeCreateEvent {
	comment: {
		collection: string;
		contentId: string;
		parentId: string | null;
		authorName: string;
		authorEmail: string;
		authorUserId: string | null;
		body: string;
		ipHash: string | null;
		userAgent: string | null;
	};
	metadata: Record<string, unknown>;
}

Valor de retorno

  • Retorne o evento modificado para transformá-lo
  • Retorne false para rejeitar
  • Retorne void para deixar passar

comment:moderate

Capability: users:read | Exclusivo: Sim

Decide se um comentário está aprovado, pendente ou é spam. Apenas um provedor de moderação fica ativo.

hooks: {
  "comment:moderate": {
    exclusive: true,
    handler: async (event, ctx) => {
      const score = await checkSpam(event.comment);
      return {
        status: score > 0.8 ? "spam" : score > 0.5 ? "pending" : "approved",
        reason: `Spam score: ${score}`,
      };
    },
  },
}

Evento

interface CommentModerateEvent {
	comment: { /* same as beforeCreate */ };
	metadata: Record<string, unknown>;
	collectionSettings: {
		commentsEnabled: boolean;
		commentsModeration: "all" | "first_time" | "none";
		commentsClosedAfterDays: number;
		commentsAutoApproveUsers: boolean;
	};
	priorApprovedCount: number;
}

Valor de retorno

{ status: "approved" | "pending" | "spam"; reason?: string }

Moderador ativo

O EmDash inclui um moderador integrado que aplica as configurações de comentários da collection. Quando exatamente um plugin fornece comment:moderate, o EmDash seleciona esse plugin no lugar do moderador integrado e armazena a escolha. Quando vários plugins o fornecem e não há escolha armazenada, o EmDash não seleciona nenhum e os novos comentários aguardam revisão. Uma escolha armazenada, inclusive a do moderador integrado, permanece em vigor enquanto o moderador selecionado estiver ativado.

comment:afterCreate

Capability: users:read

Hook fire-and-forget executado depois de um comentário ser armazenado. Use-o para notificações. Enviar e-mail também exige a capability email:send e um provedor email:deliver configurado; sem os dois, ctx.email é undefined.

hooks: {
  "comment:afterCreate": async (event, ctx) => {
    const recipient = event.contentAuthor?.email;
    if (event.comment.status === "approved" && recipient && ctx.email) {
      await ctx.email.send({
        to: recipient,
        subject: `New comment on "${event.content.title}"`,
        text: `${event.comment.authorName} commented: ${event.comment.body}`,
      });
    }
  },
}

Evento e valor de retorno

interface CommentAfterCreateEvent {
	comment: StoredComment;
	metadata: Record<string, unknown>;
	content: { id: string; collection: string; slug: string; title?: string };
	contentAuthor?: { id: string; name: string | null; email: string };
}

O hook não tem valor de retorno.

comment:afterModerate

Capability: users:read

É executado depois de um administrador ou um plugin alterar o status de um comentário. Uma transição bem-sucedida executa o hook uma vez. Os erros do hook são registrados em log e não desfazem a mudança de status.

Evento

interface CommentAfterModerateEvent {
	comment: StoredComment;
	previousStatus: string;
	newStatus: string;
	moderator: { id: string; name: string | null };
	origin?:
		| { source: "admin"; userId: string }
		| { source: "plugin"; pluginId: string };
}

O hook não tem valor de retorno.

Hooks de byline

Os hooks de byline são executados depois de a API de administração ou uma ferramenta MCP alterar o perfil de um byline. Não são executados para seeds nem importações. Use-os para manter atualizada uma cópia externa dos dados de autores, como um índice de busca. Os erros do hook são registrados em log e não desfazem a alteração.

Revincular um byline a outro usuário pode alterar os créditos inferidos para as entradas desse usuário. Nenhum hook por entrada é executado para essa alteração.

byline:afterSave

Capability: bylines:read

É executado depois de um byline ou de uma tradução de byline ser criado ou atualizado.

hooks: {
  "byline:afterSave": async (event, ctx) => {
    await ctx.storage.authors.put(event.byline.id, {
      name: event.byline.displayName,
      slug: event.byline.slug,
    });
  },
}

Evento

interface BylineAfterSaveEvent {
	byline: BylineInfo;
	/** `true` when the byline was just created. */
	isNew: boolean;
}

BylineInfo tem os mesmos campos públicos que ctx.bylines.get() retorna. Omite a conta de usuário vinculada, o indicador de convidado e os valores dos campos personalizados.

O hook não tem valor de retorno.

byline:afterDelete

Capability: bylines:read

É executado depois de um byline ser excluído. O evento carrega o byline como ele era antes da exclusão.

Evento

interface BylineAfterDeleteEvent {
	byline: BylineInfo;
}

O hook não tem valor de retorno.

Hooks de página

Os hooks de página são executados ao renderizar páginas públicas. Eles permitem que os plugins injetem metadados e scripts.

Ambos os hooks de página recebem o contexto atual da página pública:

interface PublicPageContext {
	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;
}

interface PageMetadataEvent { page: PublicPageContext }
interface PageFragmentEvent { page: PublicPageContext }

page:metadata

Capability: Nenhuma necessária

Contribua com meta tags, propriedades Open Graph, dados estruturados JSON-LD ou link tags para o head da página.

hooks: {
  "page:metadata": async (event, ctx) => {
    return [
      { kind: "meta", name: "generator", content: "EmDash" },
      { kind: "property", property: "og:site_name", content: event.page.siteName ?? "My Site" },
      { kind: "jsonld", graph: { "@type": "WebSite", name: event.page.siteName } },
    ];
  },
}

Tipos de contribuição

type PageMetadataContribution =
	| { kind: "meta"; name: string; content: string; key?: string }
	| { kind: "property"; property: string; content: string; key?: string }
	| {
			kind: "link";
			rel: "canonical" | "alternate" | "author" | "license" | "nlweb" | "site.standard.document";
			href: string;
			hreflang?: string;
			key?: string;
	  }
	| {
			kind: "jsonld";
			id?: string;
			graph: Record<string, unknown> | Array<Record<string, unknown>>;
	  };

O campo key deduplica contribuições: apenas a última contribuição com uma determinada chave é usada.

Retorne uma contribuição, um array de contribuições ou null quando o plugin não tiver nada a acrescentar.

page:fragments

Capability: hooks.page-fragments:register

Injete scripts ou HTML nas páginas. Disponível apenas para plugins nativos.

hooks: {
  "page:fragments": async (event, ctx) => {
    return [
      {
        kind: "external-script",
        placement: "body:end",
        src: "https://analytics.example.com/script.js",
        async: true,
      },
      {
        kind: "inline-script",
        placement: "head",
        code: `window.siteId = "abc123";`,
      },
    ];
  },
}

Tipos de contribuição

type PageFragmentContribution =
	| {
			kind: "external-script";
			placement: "head" | "body:start" | "body:end";
			src: string;
			async?: boolean;
			defer?: boolean;
			attributes?: Record<string, string>;
			key?: string;
		}
	| {
			kind: "inline-script";
			placement: "head" | "body:start" | "body:end";
			code: string;
			attributes?: Record<string, string>;
			key?: string;
		}
	| {
			kind: "html";
			placement: "head" | "body:start" | "body:end";
			html: string;
			key?: string;
		};

Retorne uma contribuição de fragmento, um array de contribuições ou null quando o plugin não tiver nada a acrescentar.

Configuração de hooks

Os hooks aceitam uma função handler ou um objeto de configuração:

hooks: {
  // Simple handler
  "content:afterSave": async (event, ctx) => { ... },

  // With configuration
  "content:beforeSave": {
    priority: 50,        // Lower runs first (default: 100)
    timeout: 10000,      // Max execution time in ms (default: 5000)
    dependencies: [],    // Run after these plugins
    errorPolicy: "abort", // "continue" or "abort" (default)
    handler: async (event, ctx) => { ... },
  },
}

Opções de configuração

OpçãoTipoPadrãoDescrição
prioritynumber100Ordem de execução (menor = mais cedo)
timeoutnumber5000Tempo máximo de execução em milissegundos
dependenciesstring[][]IDs de plugins que devem ser executados primeiro
errorPolicystring"abort""continue" para ignorar erros
exclusivebooleanfalseApenas um plugin pode ser o provedor ativo (para hooks de padrão provider como email:deliver, comment:moderate)

Contexto do plugin

Todos os hooks recebem um objeto de contexto com acesso às APIs do plugin:

interface PluginContext {
	plugin: { id: string; version: string };
	storage: PluginStorage;
	kv: KVAccess;
	content?: ContentAccess;
	media?: MediaAccess;
	http?: HttpAccess;
	log: LogAccess;
	site: { name: string; url: string; locale: string };
	url(path: string): string;
	users?: UserAccess;
	cron?: CronAccess;
	email?: EmailAccess;
}

Consulte Capacidades de plugins para ver a capability exigida por cada API de contexto.

site.url e url() usam o mesmo endereço dos links nos e-mails: primeiro a opção siteUrl configurada, depois a Site URL em Settings > General e, por fim, o endereço em que o site foi configurado. O EmDash lê esse valor quando o runtime inicia, então uma Site URL alterada chega aos plugins depois que o servidor reinicia ou, no Cloudflare Workers, quando novos isolates são iniciados.

Tratamento de erros

Os erros nos hooks são registrados em log e tratados de acordo com errorPolicy:

  • "abort" (padrão) — interrompe a execução e, se aplicável, faz rollback da transação
  • "continue" — registra o erro em log e segue para o próximo hook
hooks: {
  "content:beforeSave": {
    errorPolicy: "continue", // Don't block save if this fails
    handler: async (event, ctx) => {
      try {
        await ctx.http?.fetch("https://api.example.com/validate");
      } catch (error) {
        ctx.log.warn("Validation service unavailable", error);
      }
    },
  },
}

Ordem de execução

Os hooks são executados nesta ordem:

  1. Ordenados por priority (crescente)
  2. Plugins com dependencies são executados depois de suas dependências
  3. Dentro da mesma prioridade, a ordem é determinística, mas não especificada
// This runs first (priority 10)
{ priority: 10, handler: ... }

// This runs second (priority 50)
{ priority: 50, handler: ... }

// This runs last (default priority 100)
{ handler: ... }