Gli hook permettono ai plugin di intercettare e modificare il comportamento di EmDash in punti specifici del ciclo di vita di contenuti, media, e-mail, commenti e pagine.
Panoramica degli hook
La tabella seguente elenca ogni hook, cosa lo attiva, cosa può modificare e se è esclusivo:
| Hook | Trigger | Può modificare | Esclusivo |
|---|---|---|---|
content:beforeSave | Prima del salvataggio del contenuto | Dati del contenuto | No |
content:afterSave | Dopo il salvataggio del contenuto | Niente | No |
content:beforeDelete | Prima dell’eliminazione del contenuto | Può annullare | No |
content:afterDelete | Dopo l’eliminazione del contenuto | Niente | No |
content:beforePublish | Prima della pubblicazione del contenuto | Può annullare | No |
content:beforeSchedule | Prima della pianificazione del contenuto | Può annullare | No |
content:beforeUnpublish | Prima della rimozione dalla pubblicazione del contenuto | Può annullare | No |
content:afterPublish | Dopo la pubblicazione del contenuto | Niente | No |
content:afterUnpublish | Dopo la rimozione dalla pubblicazione del contenuto | Niente | No |
content:afterRestore | Dopo il ripristino del contenuto | Niente | No |
content:afterSchedule | Dopo la pianificazione del contenuto | Niente | No |
content:afterUnschedule | Dopo l’annullamento della pianificazione | Niente | No |
media:beforeUpload | Prima del caricamento di un file | Metadati del file | No |
media:afterUpload | Dopo il caricamento di un file | Niente | No |
cron | Esecuzione di un’attività pianificata | Niente | No |
email:beforeSend | Prima della consegna dell’e-mail | Messaggio, può annullare | No |
email:deliver | Consegna dell’e-mail tramite il trasporto | Niente | Sì |
email:afterSend | Dopo una consegna e-mail riuscita | Niente | No |
comment:beforeCreate | Prima della memorizzazione del commento | Commento, può annullare | No |
comment:moderate | Decidere lo stato di approvazione del commento | Stato | Sì |
comment:afterCreate | Dopo la memorizzazione del commento | Niente | No |
comment:afterModerate | Dopo la modifica dello stato del commento da parte di un admin | Niente | No |
byline:afterSave | Dopo la creazione o l’aggiornamento di un byline | Niente | No |
byline:afterDelete | Dopo l’eliminazione di un byline | Niente | No |
page:metadata | Rendering dell’head della pagina pubblica | Aggiungere tag | No |
page:fragments | Rendering del body della pagina pubblica | Iniettare script | No |
plugin:install | Alla prima installazione del plugin | Niente | No |
plugin:activate | Quando il plugin viene abilitato | Niente | No |
plugin:deactivate | Quando il plugin viene disabilitato | Niente | No |
plugin:uninstall | Quando il plugin viene rimosso | Niente | No |
Hook dei contenuti
content:beforeSave
Capability: content:write
Viene eseguito prima che il contenuto sia salvato nel database. Usalo per validare, trasformare o arricchire il contenuto. Un hook in sandbox rifiuta il salvataggio restituendo un risultato di hook versione 1 con un errore SAVE_REJECTED e un reason in testo semplice di 1–500 caratteri. L’API risponde con SAVE_REJECTED e l’admin identifica il plugin e mostra il motivo come testo. Risultati di errore vuoti, troppo lunghi, malformati o sconosciuti fanno fallire il salvataggio con una risposta generica CONTENT_HOOK_ERROR.
Dal processo host, lancia ContentSaveRejectedError (esportato da emdash) per rifiutare un salvataggio. Qualsiasi altra eccezione, in entrambe le modalità di esecuzione, fa fallire il salvataggio con una risposta generica che non espone il messaggio dell’eccezione.
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
}
In un aggiornamento, content contiene solo i valori dei campi inviati. Carica l’elemento memorizzato con ctx.content.get(event.collection, event.id) quando l’hook deve confrontarsi con esso. I salvataggi REST autenticati, di editing visuale e MCP includono actor. Le scritture interne senza utente autenticato lo omettono.
locale è il locale in cui viene salvata la voce: in una creazione, il locale richiesto risolto rispetto ai locale configurati, oppure il locale predefinito quando la richiesta non ne indica nessuno; in un aggiornamento, il locale della voce memorizzata. translationOf viene impostato solo quando una creazione proviene dal flusso di traduzione e contiene l’ID della voce che viene tradotta. Usa i due campi insieme per distinguere una nuova voce da una nuova traduzione di una voce esistente, ad esempio per consentire una sola voce per locale in una 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.");
}
},
Valore di ritorno
- Restituisci l’oggetto contenuto modificato per applicare le modifiche
- Restituisci un envelope di errore dell’hook in sandbox per rifiutare il salvataggio con un motivo in testo semplice di lunghezza limitata
- Restituisci
voidper lasciar passare senza modifiche
Un hook in sandbox restituisce questo envelope completo per rifiutare un salvataggio:
return {
__emdashSandboxHookResult: true,
version: 1,
error: {
code: "SAVE_REJECTED",
reason: "Add a summary before saving.",
},
};
L’host applica il trim a reason e accetta da 1 a 500 caratteri.
content:afterSave
Capability: content:read
Viene eseguito dopo il salvataggio del contenuto. Usalo per effetti collaterali come notifiche, invalidazione della cache o sincronizzazione esterna.
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 riceve content, collection, isNew, l’actor autenticato opzionale e gli stessi locale e translationOf di content:beforeSave. content è la voce salvata completa, con il suo ID di database in content.id e i campi della collection sotto content.data. L’id opzionale separato usato negli aggiornamenti di content:beforeSave non è presente dopo il salvataggio.
Valore di ritorno
Non è previsto alcun valore di ritorno.
content:beforeDelete
Capability: content:read
Viene eseguito prima dell’eliminazione del contenuto. Usalo per validare l’eliminazione o impedirla.
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 viene eseguito solo quando una voce viene spostata nel cestino. I plugin nativi ricevono permanent: false; il runtime in sandbox invia solo id e collection. Non ramificare in un hook in sandbox in base a questo campo. L’eliminazione definitiva aggira content:beforeDelete, quindi l’hook non può impedire a un amministratore di eliminare definitivamente una voce già nel cestino.
Valore di ritorno
- Restituisci
falseper annullare l’eliminazione - Restituisci
trueovoidper consentirla
content:afterDelete
Capability: content:read
Viene eseguito dopo l’eliminazione del contenuto. Usalo per le operazioni di pulizia.
hooks: {
"content:afterDelete": async (event, ctx) => {
const { id, collection, permanent } = event;
if (permanent) {
await ctx.storage.relatedItems.delete(`${collection}:${id}`);
}
},
}
L’evento contiene id, collection e permanent. permanent è false quando la voce viene spostata nel cestino e true quando viene eliminata definitivamente. Controllalo prima di rimuovere dati di cui una voce ripristinata dal cestino avrebbe ancora bisogno. Questo hook non ha valore di ritorno.
content:beforePublish
Capability: hooks.content-policy:register
Viene eseguito prima della pubblicazione del contenuto. Restituisci void per consentire la pubblicazione oppure { cancel: true, reason } per rifiutarla. EmDash accetta 1–500 caratteri in testo semplice in reason e restituisce PUBLISH_REJECTED in caso di annullamento esplicito.
L’evento contiene content, collection e origin correnti. Le azioni autenticate di API, MCP ed editor visuale includono anche actor con il relativo source. L’origine visual-editor richiede il token di azione firmato e di breve durata proveniente da un render autenticato della barra degli strumenti. Le voci pianificate rieseguono questo hook quando diventano dovute. Un rifiuto dello scheduler annulla la pianificazione ed elenca nella dashboard la voce interessata e il motivo. Una pianificazione, pubblicazione o eliminazione riuscita cancella il record; un amministratore può chiudere un record obsoleto.
content:beforeSchedule
Capability: hooks.content-policy:register
Viene eseguito prima che una voce riceva un orario di pubblicazione. Usa lo stesso formato di decisione e gli stessi campi di origine di content:beforePublish, aggiunge scheduledAt all’evento e restituisce SCHEDULE_REJECTED in caso di annullamento esplicito.
Non esiste un hook content:beforeUnschedule. Un amministratore può sempre annullare una pubblicazione futura.
content:beforeUnpublish
Capability: hooks.content-policy:register
Viene eseguito prima di rimuovere un contenuto pubblicato online. Usa lo stesso formato di decisione e gli stessi campi di origine di content:beforePublish e restituisce UNPUBLISH_REJECTED in caso di annullamento esplicito.
hooks.content-policy:register non implica content:read, content:write né azioni di pubblicazione. Le decisioni non valide e gli errori imprevisti della policy abort falliscono in modalità chiusa, senza esporre i messaggi delle eccezioni.
content:afterPublish
Viene eseguito dopo che una voce è stata pubblicata con successo, compresa una voce che EmDash pubblica automaticamente all’orario pianificato. Usalo per il lavoro che dipende dalla voce pubblicata, come notificare un altro servizio o aggiornare un indice di ricerca esterno.
hooks: {
"content:afterPublish": async (event, ctx) => {
ctx.log.info(`Published ${event.collection}/${event.content.id}`);
},
}
L’hook richiede la capability content:read. EmDash lo esegue dopo la risposta di pubblicazione, quindi il suo valore di ritorno non può modificare la voce né annullare la pubblicazione. Un errore viene registrato; con errorPolicy: "abort", gli hook di pubblicazione successivi non vengono eseguiti.
content:afterUnpublish
Viene eseguito dopo che una voce è stata rimossa dalla pubblicazione con successo. Usalo per rimuovere o aggiornare copie del contenuto presenti in sistemi esterni. La rimozione dalla pubblicazione annulla anche eventuali pianificazioni in sospeso, senza eseguire content:afterUnschedule.
hooks: {
"content:afterUnpublish": async (event, ctx) => {
ctx.log.info(`Unpublished ${event.collection}/${event.content.id}`);
},
}
Questo hook ha lo stesso requisito di capability content:read, la stessa esecuzione differita, la stessa forma dell’evento e lo stesso contratto senza valore di ritorno di content:afterPublish.
content:afterRestore
Viene eseguito dopo il ripristino di un contenuto dal cestino. Richiede la capability content:read. La voce ripristinata è una bozza senza pianificazione, qualunque fosse il suo stato quando è stata cestinata. L’eliminazione della pianificazione non esegue anche content:afterUnschedule.
hooks: {
"content:afterRestore": async (event, ctx) => {
ctx.log.info(`Restored ${event.collection}/${event.content.id}`);
},
}
content:afterSchedule
Viene eseguito dopo che un contenuto è stato pianificato per una pubblicazione futura. Richiede la capability content:read.
hooks: {
"content:afterSchedule": async (event, ctx) => {
ctx.log.info(`Scheduled ${event.collection}/${event.content.id}`);
},
}
content:afterUnschedule
Viene eseguito dopo che la pianificazione di un contenuto è stata annullata. Richiede la 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;
}
Questa forma di evento è condivisa da content:afterPublish, content:afterUnpublish, content:afterRestore, content:afterSchedule e content:afterUnschedule. content è la voce completa dopo il cambio di stato, inclusi id, slug e stato; i campi della collection si trovano sotto content.data.
Valore di ritorno
Non è previsto alcun valore di ritorno.
Hook dei media
media:beforeUpload
Capability: media:write
Viene eseguito prima del caricamento di un file. Usalo per validare, rinominare o rifiutare i file.
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
};
}
Valore di ritorno
- Restituisci i metadati del file modificati per applicare le modifiche
- Restituisci
voidper lasciar passare senza modifiche - Lancia un’eccezione per rifiutare il caricamento
media:afterUpload
Capability: media:read
Viene eseguito dopo il caricamento di un file. Usalo per elaborazione, miniature o estrazione di metadati.
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;
};
}
Valore di ritorno
Non è previsto alcun valore di ritorno.
Hook del ciclo di vita
Gli hook del ciclo di vita non richiedono alcuna capability di registrazione.
plugin:install
Viene eseguito quando un plugin viene installato per la prima volta. Usalo per la configurazione iniziale, la creazione di collection di storage o i dati 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
Viene eseguito quando un plugin viene abilitato (dopo l’installazione o una riattivazione).
hooks: {
"plugin:activate": async (event, ctx) => {
ctx.log.info("Plugin activated");
},
}
plugin:deactivate
Viene eseguito quando un plugin viene disabilitato.
hooks: {
"plugin:deactivate": async (event, ctx) => {
ctx.log.info("Plugin deactivated");
},
}
plugin:install, plugin:activate e plugin:deactivate ricevono un oggetto evento vuoto. Non hanno valore di ritorno.
plugin:uninstall
Viene eseguito quando un plugin viene rimosso. Usalo per la pulizia.
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
}
L’hook di disinstallazione non ha valore di ritorno.
Hook cron
cron
Capability: Nessuna richiesta
Viene attivato quando un’attività pianificata viene eseguita. Pianifica le attività con ctx.cron.schedule(). Le espressioni cron sono valutate in UTC su ogni 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;
}
L’hook cron non ha valore di ritorno.
Hook e-mail
Per i messaggi inviati dai plugin, gli hook e-mail vengono eseguiti in quest’ordine: email:beforeSend, poi email:deliver, poi email:afterSend. I messaggi di autenticazione di sistema vanno direttamente a email:deliver; non passano da email:beforeSend né da email:afterSend.
email:beforeSend
Capability: hooks.email-events:register
Hook middleware prima della consegna. Trasforma i messaggi o annulla la consegna.
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;
}
Valore di ritorno
- Restituisci il messaggio modificato per trasformarlo
- Restituisci
falseper annullare la consegna
email:deliver
Capability: hooks.email-transport:register | Esclusivo: Sì
Il provider di trasporto. Solo un plugin può consegnare le e-mail. È responsabile dell’invio effettivo del messaggio tramite un servizio e-mail.
hooks: {
"email:deliver": {
exclusive: true,
handler: async (event, ctx) => {
await sendViaSES(event.message);
},
},
}
Evento e valore di ritorno
interface EmailDeliverEvent {
message: {
to: string;
cc?: string[];
replyTo?: string;
subject: string;
text: string;
html?: string;
};
source: string;
}
L’hook non ha valore di ritorno. source è "system" per i messaggi di autenticazione di EmDash e l’ID del plugin per i messaggi inviati da un plugin.
I plugin possono impostare cc (destinatari aggiuntivi) e replyTo su un messaggio. I provider devono consegnare entrambi quando presenti; un provider che scarta cc in silenzio lascia quei destinatari senza il messaggio.
email:afterSend
Capability: hooks.email-events:register
Hook fire-and-forget dopo una consegna riuscita. Gli errori vengono registrati ma non si propagano.
hooks: {
"email:afterSend": async (event, ctx) => {
await ctx.kv.set(`email:log:${Date.now()}`, {
to: event.message.to,
subject: event.message.subject,
});
},
}
Evento e valore di ritorno
email:afterSend riceve gli stessi campi message e source di email:deliver. Non ha valore di ritorno.
Hook dei commenti
Gli hook dei commenti vengono eseguiti in quest’ordine: comment:beforeCreate, poi comment:moderate, poi comment:afterCreate. L’hook comment:afterModerate viene eseguito separatamente quando un amministratore o un plugin autorizzato cambia lo stato di un commento.
Dopo la memorizzazione di un commento, gli hook ricevono questa forma di record:
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 prima della memorizzazione di un commento. Arricchisci, valida o rifiuta i commenti.
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>;
}
Valore di ritorno
- Restituisci l’evento modificato per trasformarlo
- Restituisci
falseper rifiutare - Restituisci
voidper lasciar passare
comment:moderate
Capability: users:read | Esclusivo: Sì
Decide se un commento è approvato, in attesa o spam. È attivo un solo provider di moderazione.
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;
}
Valore di ritorno
{ status: "approved" | "pending" | "spam"; reason?: string }
Moderatore attivo
EmDash include un moderatore integrato che applica le impostazioni dei commenti della collection. Quando esattamente un plugin fornisce comment:moderate, EmDash seleziona quel plugin al posto del moderatore integrato e memorizza la scelta. Quando più plugin lo forniscono e non c’è alcuna scelta memorizzata, EmDash non ne seleziona nessuno e i nuovi commenti restano in attesa di revisione. Una scelta memorizzata, compresa quella del moderatore integrato, rimane valida finché il moderatore selezionato è abilitato.
comment:afterCreate
Capability: users:read
Hook fire-and-forget dopo la memorizzazione di un commento. Usalo per le notifiche. L’invio di e-mail richiede anche la capability email:send e un provider email:deliver configurato; senza entrambi, 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 valore di ritorno
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 };
}
L’hook non ha valore di ritorno.
comment:afterModerate
Capability: users:read
Viene eseguito dopo che un amministratore o un plugin ha cambiato lo stato di un commento. Una transizione riuscita esegue l’hook una volta. Gli errori dell’hook vengono registrati e non annullano il cambio di stato.
Evento
interface CommentAfterModerateEvent {
comment: StoredComment;
previousStatus: string;
newStatus: string;
moderator: { id: string; name: string | null };
origin?:
| { source: "admin"; userId: string }
| { source: "plugin"; pluginId: string };
}
L’hook non ha valore di ritorno.
Hook dei byline
Gli hook dei byline vengono eseguiti dopo che l’API di amministrazione o uno strumento MCP modifica il profilo di un byline. Non vengono eseguiti per seed o importazioni. Usali per mantenere aggiornata una copia esterna dei dati degli autori, come un indice di ricerca. Gli errori dell’hook vengono registrati e non annullano la modifica.
Ricollegare un byline a un altro utente può cambiare i crediti dedotti per le voci di quell’utente. Per questa modifica non viene eseguito alcun hook per singola voce.
byline:afterSave
Capability: bylines:read
Viene eseguito dopo che un byline o la traduzione di un byline è stato creato o aggiornato.
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 ha gli stessi campi pubblici restituiti da ctx.bylines.get(). Omette l’account utente collegato, il flag ospite e i valori dei campi personalizzati.
L’hook non ha valore di ritorno.
byline:afterDelete
Capability: bylines:read
Viene eseguito dopo l’eliminazione di un byline. L’evento contiene il byline così com’era prima dell’eliminazione.
Evento
interface BylineAfterDeleteEvent {
byline: BylineInfo;
}
L’hook non ha valore di ritorno.
Hook di pagina
Gli hook di pagina vengono eseguiti durante il rendering delle pagine pubbliche. Permettono ai plugin di iniettare metadati e script.
Entrambi gli hook di pagina ricevono il contesto corrente della pagina pubblica:
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: Nessuna richiesta
Aggiungi meta tag, proprietà Open Graph, dati strutturati JSON-LD o link tag all’head della pagina.
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 } },
];
},
}
Tipi di contributo
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>>;
};
Il campo key deduplica i contributi: viene usato solo l’ultimo contributo con una data chiave.
Restituisci un contributo, un array di contributi oppure null quando il plugin non ha nulla da aggiungere.
page:fragments
Capability: hooks.page-fragments:register
Inietta script o HTML nelle pagine. Disponibile solo per i plugin nativi.
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";`,
},
];
},
}
Tipi di contributo
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;
};
Restituisci un contributo di frammento, un array di contributi oppure null quando il plugin non ha nulla da aggiungere.
Configurazione degli hook
Gli hook accettano una funzione handler oppure un oggetto di configurazione:
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) => { ... },
},
}
Opzioni di configurazione
| Opzione | Tipo | Predefinito | Descrizione |
|---|---|---|---|
priority | number | 100 | Ordine di esecuzione (più basso = prima) |
timeout | number | 5000 | Tempo massimo di esecuzione in millisecondi |
dependencies | string[] | [] | ID plugin che devono essere eseguiti per primi |
errorPolicy | string | "abort" | "continue" per ignorare gli errori |
exclusive | boolean | false | Un solo plugin può essere il provider attivo (hook con pattern provider come email:deliver, comment:moderate) |
Contesto del plugin
Tutti gli hook ricevono un oggetto di contesto con accesso alle API del 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;
}
Vedi Capability dei plugin per la capability richiesta da ciascuna API di contesto.
Gestione degli errori
Gli errori negli hook vengono registrati e gestiti in base a errorPolicy:
"abort"(predefinito) — interrompe l’esecuzione e, se applicabile, esegue il rollback della transazione"continue"— registra l’errore e prosegue con l’hook successivo
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);
}
},
},
}
Ordine di esecuzione
Gli hook vengono eseguiti in quest’ordine:
- Ordinati per
priority(crescente) - I plugin con
dependenciesvengono eseguiti dopo le loro dipendenze - A parità di priorità, l’ordine è deterministico ma non specificato
// This runs first (priority 10)
{ priority: 10, handler: ... }
// This runs second (priority 50)
{ priority: 50, handler: ... }
// This runs last (default priority 100)
{ handler: ... }