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:
| Hook | Gatilho | Pode modificar | Exclusivo |
|---|---|---|---|
content:beforeSave | Antes de o conteúdo ser salvo | Dados do conteúdo | Não |
content:afterSave | Depois de o conteúdo ser salvo | Nada | Não |
content:beforeDelete | Antes de o conteúdo ser excluído | Pode cancelar | Não |
content:afterDelete | Depois de o conteúdo ser excluído | Nada | Não |
content:beforePublish | Antes de o conteúdo ser publicado | Pode cancelar | Não |
content:beforeSchedule | Antes de o conteúdo ser agendado | Pode cancelar | Não |
content:beforeUnpublish | Antes de o conteúdo ser despublicado | Pode cancelar | Não |
content:afterPublish | Depois de o conteúdo ser publicado | Nada | Não |
content:afterUnpublish | Depois de o conteúdo ser despublicado | Nada | Não |
content:afterRestore | Depois de o conteúdo ser restaurado | Nada | Não |
content:afterSchedule | Depois de o conteúdo ser agendado | Nada | Não |
content:afterUnschedule | Depois de o agendamento ser cancelado | Nada | Não |
media:beforeUpload | Antes de um arquivo ser enviado | Metadados do arquivo | Não |
media:afterUpload | Depois de um arquivo ser enviado | Nada | Não |
cron | Uma tarefa agendada é disparada | Nada | Não |
email:beforeSend | Antes da entrega do e-mail | Mensagem, pode cancelar | Não |
email:deliver | Entregar o e-mail por meio do transporte | Nada | Sim |
email:afterSend | Depois de uma entrega de e-mail bem-sucedida | Nada | Não |
comment:beforeCreate | Antes de o comentário ser armazenado | Comentário, pode cancelar | Não |
comment:moderate | Decidir o status de aprovação do comentário | Status | Sim |
comment:afterCreate | Depois de o comentário ser armazenado | Nada | Não |
comment:afterModerate | Depois de um admin alterar o status do comentário | Nada | Não |
byline:afterSave | Depois de um byline ser criado ou atualizado | Nada | Não |
byline:afterDelete | Depois de um byline ser excluído | Nada | Não |
page:metadata | Renderização do head da página pública | Contribuir com tags | Não |
page:fragments | Renderização do body da página pública | Injetar scripts | Não |
plugin:install | Quando o plugin é instalado pela primeira vez | Nada | Não |
plugin:activate | Quando o plugin é ativado | Nada | Não |
plugin:deactivate | Quando o plugin é desativado | Nada | Não |
plugin:uninstall | Quando o plugin é removido | Nada | Nã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
voidpara 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
falsepara cancelar a exclusão - Retorne
trueouvoidpara 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
voidpara 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
falsepara 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
falsepara rejeitar - Retorne
voidpara 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ção | Tipo | Padrão | Descrição |
|---|---|---|---|
priority | number | 100 | Ordem de execução (menor = mais cedo) |
timeout | number | 5000 | Tempo máximo de execução em milissegundos |
dependencies | string[] | [] | IDs de plugins que devem ser executados primeiro |
errorPolicy | string | "abort" | "continue" para ignorar erros |
exclusive | boolean | false | Apenas 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:
- Ordenados por
priority(crescente) - Plugins com
dependenciessão executados depois de suas dependências - 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: ... }