Hooks

In questa pagina

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 — il PluginContext con 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

OpzioneTipoPredefinitoDescrizione
prioritynumber100Ordine di esecuzione. I numeri più bassi vengono eseguiti per primi.
timeoutnumber5000Tempo massimo di esecuzione in millisecondi.
dependenciesstring[][]ID dei plugin che devono essere eseguiti prima di questo hook.
errorPolicy"abort" | "continue""abort"Se interrompere la pipeline in caso di errore.
exclusivebooleanfalseSolo un plugin può essere il fornitore attivo. Usato per email:deliver e comment:moderate.
handlerfunctionLa 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:

TipoRenderizzaChiave 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:

  1. Gli hook con valori di priority più bassi vengono eseguiti per primi.
  2. Per priorità uguali, gli hook vengono eseguiti nell’ordine di registrazione del plugin.
  3. Gli hook con dependencies attendono 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

HookTriggerRitornoEsclusivo
plugin:installPrima installazione pluginvoidNo
plugin:activatePlugin abilitatovoidNo
plugin:deactivatePlugin disabilitatovoidNo
plugin:uninstallPlugin rimossovoidNo
content:beforeSavePrima del salvataggioContenuto modificato o voidNo
content:afterSaveDopo il salvataggiovoidNo
content:beforeDeletePrima dell’eliminazionefalse per annullare, altrimenti okNo
content:afterDeleteDopo l’eliminazionevoidNo
content:afterPublishDopo la pubblicazionevoidNo
content:afterUnpublishDopo la depubblicazionevoidNo
content:afterRestoreDopo il ripristinovoidNo
content:afterScheduleDopo la programmazionevoidNo
content:afterUnscheduleDopo la deprogrammazionevoidNo
media:beforeUploadPrima del caricamento fileInfo file modificata o voidNo
media:afterUploadDopo il caricamento filevoidNo
cronAttività pianificata attivatavoidNo
email:beforeSendPrima dell’invio emailMessaggio modificato, false o voidNo
email:deliverConsegna email via trasportovoid
email:afterSendDopo l’invio emailvoidNo
comment:beforeCreatePrima dello storage commentoEvento modificato, false o voidNo
comment:moderateDecisione stato commento{ status, reason? }
comment:afterCreateDopo lo storage commentovoidNo
comment:afterModerateAdmin cambia stato commentovoidNo
page:metadataRendering paginaContributi o nullNo
page:fragmentsRendering pagina (solo nativo)Contributi o nullNo

Vedi il Riferimento degli Hook per i tipi di evento completi e le firme degli handler.