Gli hooks permettono ai plugin di eseguire codice in risposta a eventi. Tutti gli hooks ricevono un oggetto evento e il contesto del plugin, e vengono dichiarati al momento della definizione del plugin — non c’è registrazione dinamica a runtime.
Questa pagina tratta i plugin sandboxed. Gli hooks funzionano in modo identico nei plugin nativi; i plugin nativi possono inoltre registrare page:fragments.
Firma dell’hook
Ogni handler di hook prende due argomenti:
async (event, ctx) => ReturnType;
event— dati su ciò che è appena accaduto (contenuto in fase di salvataggio, media caricati, transizione del ciclo di vita, ecc.)ctx— ilPluginContextcon storage, KV, logging e API controllate da capacità
satisfies SandboxedPlugin sull’export predefinito inferisce event dal nome dell’hook (il tipo di evento canonico completo) e ctx come PluginContext, quindi gli handler non necessitano di annotazioni dei parametri. Per riferirsi a un tipo di evento per nome in un helper, importalo da emdash/plugin.
Configurazione dell’hook
Un hook può essere dichiarato come handler semplice o racchiuso in un oggetto di configurazione:
Semplice
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved");
},
}, Configurazione completa
hooks: {
"content:afterSave": {
priority: 100,
timeout: 5000,
dependencies: ["audit-log"],
errorPolicy: "continue",
handler: async (event, ctx) => {
ctx.log.info("Content saved");
},
},
}, Opzioni di configurazione
| Opzione | Tipo | Predefinito | Descrizione |
|---|---|---|---|
priority | number | 100 | Ordine di esecuzione. I numeri più bassi vengono eseguiti per primi. |
timeout | number | 5000 | Tempo massimo di esecuzione in millisecondi. |
dependencies | string[] | [] | ID dei plugin che devono essere eseguiti prima di questo hook. |
errorPolicy | "abort" | "continue" | "abort" | Se interrompere la pipeline in caso di errore. |
exclusive | boolean | false | Solo un plugin può essere il fornitore attivo. Usato per email:deliver e comment:moderate. |
handler | function | — | La funzione handler dell’hook. Obbligatoria. |
Hook del ciclo di vita
Vengono eseguiti durante l’installazione, l’attivazione, la disattivazione e la rimozione del plugin.
plugin:install
Viene eseguito una volta quando il plugin viene aggiunto a un sito per la prima volta.
"plugin:install": async (_event, ctx) => {
ctx.log.info("Installing plugin...");
await ctx.kv.set("settings:enabled", true);
await ctx.storage.items.put("default", { name: "Default Item" });
},
Evento: {} — Restituisce: Promise<void>
plugin:activate
Viene eseguito quando il plugin viene abilitato (dopo l’installazione o quando viene riabilitato).
"plugin:activate": async (_event, ctx) => {
ctx.log.info("Plugin activated");
},
Evento: {} — Restituisce: Promise<void>
plugin:deactivate
Viene eseguito quando il plugin viene disabilitato (ma non rimosso).
"plugin:deactivate": async (_event, ctx) => {
ctx.log.info("Plugin deactivated");
},
Evento: {} — Restituisce: Promise<void>
plugin:uninstall
Viene eseguito quando il plugin viene rimosso da un sito.
"plugin:uninstall": async (event, ctx) => {
ctx.log.info("Uninstalling plugin...");
if (event.deleteData) {
const result = await ctx.storage.items.query({ limit: 1000 });
await ctx.storage.items.deleteMany(result.items.map((i) => i.id));
}
},
Evento: { deleteData: boolean } — Restituisce: Promise<void>
Hook di contenuto
Vengono eseguiti durante le operazioni di creazione, aggiornamento ed eliminazione sui contenuti del sito.
content:beforeSave
Viene eseguito prima del salvataggio del contenuto. Restituisce il contenuto modificato, o void per lasciarlo invariato. Lancia un’eccezione per annullare.
"content:beforeSave": async (event, ctx) => {
const { content, collection } = event;
if (collection === "posts" && !content.title) {
throw new Error("Posts require a title");
}
if (typeof content.slug === "string") {
content.slug = content.slug.toLowerCase().replace(/\s+/g, "-");
}
return content;
},
Evento: { content, collection, isNew } — Restituisce: contenuto modificato o void.
content:afterSave
Viene eseguito dopo il salvataggio riuscito del contenuto. Usalo per effetti collaterali come notifiche, logging o sincronizzazioni esterne.
"content:afterSave": async (event, ctx) => {
ctx.log.info(`${event.isNew ? "Created" : "Updated"} ${event.collection}/${event.content.id}`);
if (ctx.http) {
await ctx.http.fetch("https://api.example.com/webhook", {
method: "POST",
body: JSON.stringify({ event: "content:save", id: event.content.id }),
});
}
},
Evento: { content, collection, isNew } — Restituisce: Promise<void>
content:beforeDelete
Viene eseguito prima dell’eliminazione del contenuto. Restituisce false per annullare; true o void lo permette.
"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 } — Restituisce: boolean | void
content:afterDelete
Viene eseguito dopo l’eliminazione riuscita del contenuto.
"content:afterDelete": async (event, ctx) => {
await ctx.storage.cache.delete(`${event.collection}:${event.id}`);
},
Evento: { id, collection } — Restituisce: Promise<void>
content:afterPublish
Viene eseguito dopo la promozione del contenuto da bozza a pubblicato. Richiede la capacità content:read.
Evento: { content, collection } — Restituisce: Promise<void>
content:afterUnpublish
Viene eseguito dopo il ritorno del contenuto da pubblicato a bozza. Richiede la capacità content:read.
Evento: { content, collection } — Restituisce: Promise<void>
content:afterRestore
Viene eseguito dopo il ripristino del contenuto eliminato. Richiede la capacità content:read.
Evento: { content, collection } — Restituisce: Promise<void>
content:afterSchedule
Viene eseguito dopo la programmazione del contenuto per pubblicazione futura. Richiede la capacità content:read.
Evento: { content, collection } — Restituisce: Promise<void>
content:afterUnschedule
Viene eseguito dopo la deprogrammazione del contenuto pianificato. Richiede la capacità content:read.
Evento: { content, collection } — Restituisce: Promise<void>
Hook dei media
media:beforeUpload
Viene eseguito prima del caricamento di un file. Restituisce metadati del file modificati o lancia un’eccezione per annullare.
"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 } } — Restituisce: file modificato o void
media:afterUpload
Viene eseguito dopo il caricamento riuscito di un file.
Evento: { media: { id, filename, mimeType, size, url, createdAt } } — Restituisce: Promise<void>
Hook delle pagine pubbliche
Questi permettono ai plugin di contribuire alle pagine pubbliche renderizzate. I template optano per questo includendo i componenti <EmDashHead>, <EmDashBodyStart> e <EmDashBodyEnd> da emdash/ui.
page:metadata
Contribuisce metadati tipizzati a <head> — tag meta, proprietà OpenGraph, rel <link> consentiti e JSON-LD. Disponibile sia per plugin sandboxed che nativi. Core valida, deduplica e renderizza i contributi; i plugin restituiscono dati strutturati, mai HTML grezzo.
"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 };
}
}
Restituisce: PageMetadataContribution | PageMetadataContribution[] | null
Tipi di contributo:
| Tipo | Renderizza | Chiave di deduplicazione |
|---|---|---|
meta | <meta name="..." content="..."> | key o name |
property | <meta property="..." content="..."> | key o property |
link | <link rel="canonical|alternate" href="..."> | canonical: singleton; alternate: key o hreflang |
jsonld | <script type="application/ld+json"> | id (se presente) |
Il primo contributo vince per ogni chiave di deduplicazione. Il rel del link è limitato a una lista consentita di sicurezza (canonical, alternate, author, license, nlweb, site.standard.document); href deve essere HTTP o HTTPS.
page:fragments
Contribuisce HTML grezzo, script o fogli di stile ai punti di inserimento della pagina. Solo plugin nativi.
I plugin sandboxed non possono usare questo hook perché il suo output viene eseguito come codice first-party nel browser del visitatore, al di fuori di qualsiasi confine sandbox. Per contributi di pagina sicuri per il sandbox, usa page:metadata. Vedi Plugin nativi: page fragments se hai bisogno di questa superficie.
Ordine di esecuzione degli hook
Gli hook vengono eseguiti in questo ordine:
- Gli hook con valori di
prioritypiù bassi vengono eseguiti per primi. - Per priorità uguali, gli hook vengono eseguiti nell’ordine di registrazione del plugin.
- Gli hook con
dependenciesattendono il completamento di quei plugin.
// 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"], // attende A anche se la sua priorità sarebbe normalmente successiva
handler: async () => {},
}
Gestione degli errori
Quando un hook lancia un’eccezione o scade:
errorPolicy: "abort"— l’intera pipeline si ferma e l’operazione originaria può fallire.errorPolicy: "continue"— l’errore viene registrato e gli hook rimanenti continuano a essere eseguiti.
"content:afterSave": {
timeout: 5000,
errorPolicy: "continue",
handler: async (event, ctx) => {
await ctx.http!.fetch("https://unreliable-api.com/notify");
},
},
Timeout
Gli hook hanno un timeout predefinito di 5000ms. Aumenta il timeout per lavori più lenti:
"content:afterSave": {
timeout: 30000,
handler: async (event, ctx) => {
// Operazione di lunga durata
},
},
Riferimento degli hook
| Hook | Trigger | Ritorno | Esclusivo |
|---|---|---|---|
plugin:install | Prima installazione plugin | void | No |
plugin:activate | Plugin abilitato | void | No |
plugin:deactivate | Plugin disabilitato | void | No |
plugin:uninstall | Plugin rimosso | void | No |
content:beforeSave | Prima del salvataggio | Contenuto modificato o void | No |
content:afterSave | Dopo il salvataggio | void | No |
content:beforeDelete | Prima dell’eliminazione | false per annullare, altrimenti ok | No |
content:afterDelete | Dopo l’eliminazione | void | No |
content:afterPublish | Dopo la pubblicazione | void | No |
content:afterUnpublish | Dopo la depubblicazione | void | No |
content:afterRestore | Dopo il ripristino | void | No |
content:afterSchedule | Dopo la programmazione | void | No |
content:afterUnschedule | Dopo la deprogrammazione | void | No |
media:beforeUpload | Prima del caricamento file | Info file modificata o void | No |
media:afterUpload | Dopo il caricamento file | void | No |
cron | Attività pianificata attivata | void | No |
email:beforeSend | Prima dell’invio email | Messaggio modificato, false o void | No |
email:deliver | Consegna email via trasporto | void | Sì |
email:afterSend | Dopo l’invio email | void | No |
comment:beforeCreate | Prima dello storage commento | Evento modificato, false o void | No |
comment:moderate | Decisione stato commento | { status, reason? } | Sì |
comment:afterCreate | Dopo lo storage commento | void | No |
comment:afterModerate | Admin cambia stato commento | void | No |
page:metadata | Rendering pagina | Contributi o null | No |
page:fragments | Rendering pagina (solo nativo) | Contributi o null | No |
Vedi il Riferimento degli Hook per i tipi di evento completi e le firme degli handler.