Hook

In questa pagina

Gli hook permettono ai plugin di eseguire codice in risposta a eventi. Tutti gli hook ricevono un oggetto evento e il contesto del plugin, e vengono dichiarati al momento della definizione del plugin: non esiste una registrazione dinamica a runtime.

Questa pagina riguarda i plugin sandboxed. I plugin nativi usano gli stessi nomi di hook e gli stessi tipi di evento, ma passano dalla pipeline di hook in-process e possono inoltre registrare page:fragments. Più avanti sono descritti il rifiuto dei salvataggi dalla sandbox e il comportamento in caso di errore dei runner isolati.

Firma dell’hook

Ogni handler di hook riceve due argomenti:

async (event, ctx) => ReturnType;
  • event — dati su ciò che è appena accaduto (contenuto in fase di salvataggio, media caricato, transizione del ciclo di vita, ecc.)
  • ctx — il PluginContext con storage, KV, logging e API protette da capability

Assegnando la definizione a una costante di tipo SandboxedPlugin, event viene dedotto dal nome dell’hook (il tipo di evento canonico completo) e ctx come PluginContext, quindi gli handler non hanno bisogno di annotazioni sui parametri. Esporta quella costante come export predefinito. Per fare riferimento a un tipo di evento per nome in una funzione di supporto, importalo da emdash/plugin.

Configurazione dell’hook

Un hook può essere dichiarato come semplice handler oppure racchiuso in un oggetto di configurazione. Preferisci la forma semplice, a meno che il plugin non supporti anche l’esecuzione deliberata in-process e abbia bisogno dei metadati descritti di seguito.

Semplice

hooks: {
	"content:afterSave": async (event, ctx) => {
		ctx.log.info("Content saved");
	},
},

Configurazione completa

hooks: {
	"content:afterSave": {
		priority: 100,
		timeout: 5000,
		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.
exclusivebooleanfalseSolo un plugin può essere il provider attivo. Usato per email:deliver e comment:moderate.
handlerfunction—La funzione handler dell’hook. Obbligatoria.

Capability richieste

Diversi hook espongono dati protetti o possono modificare un’operazione. EmDash li registra solo quando il manifesto dichiara la capability corrispondente:

HookCapabilityMotivo
content:beforeSavecontent:writeL’hook può sostituire il contenuto inviato.
content:beforePublish, content:beforeSchedule, content:beforeUnpublishhooks.content-policy:registerGli hook possono rifiutare le modifiche allo stato di pubblicazione.
Altri hook content:*content:readI loro eventi espongono contenuto o identificano una voce.
media:beforeUploadmedia:writeL’hook può sostituire i metadati del caricamento o interromperlo.
media:afterUploadmedia:readIl suo evento espone l’elemento media memorizzato.
email:beforeSend, email:afterSendhooks.email-events:registerGli hook ispezionano gli eventi del ciclo di vita delle e-mail.
email:deliverhooks.email-transport:registerL’hook diventa un provider di trasporto e-mail.
Tutti gli hook comment:*users:readGli eventi dei commenti possono contenere i recapiti dell’autore e metadati della richiesta.
page:fragmentshooks.page-fragments:registerL’hook inietta contenuto di pagina di prima parte (first-party) ed è solo nativo.

Gli hook del ciclo di vita, cron e page:metadata non hanno una capability di registrazione. Dichiara la capability indicata anche quando un hook si limita a leggere il proprio evento e non chiama l’API ctx corrispondente. La dichiarazione fornisce all’operatore una richiesta di consenso accurata, protegge l’API ctx ed è obbligatoria quando il plugin viene eseguito in-process. Capability e sicurezza spiega l’effetto a runtime.

Hook di ciclo di vita

Vengono eseguiti durante l’installazione, l’attivazione, la disattivazione e la rimozione del plugin.

plugin:install

Viene eseguito una sola volta, quando il plugin viene aggiunto per la prima volta a un sito.

Questo esempio presuppone che il manifesto dichiari una collection di storage items:

"plugin:install": async (_event, ctx) => {
	ctx.log.info("Installing plugin...");
	await ctx.settings.set("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 alla riabilitazione).

"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) {
		while (true) {
			const result = await ctx.storage.items.query({ limit: 100 });
			if (result.items.length === 0) break;
			await ctx.storage.items.deleteMany(result.items.map((item) => item.id));
		}
	}
},

Evento: { deleteData: boolean } — Restituisce: Promise<void>

Hook di contenuto

Vengono eseguiti durante le operazioni di creazione, aggiornamento ed eliminazione del contenuto del sito.

content:beforeSave

Viene eseguito prima che il contenuto venga salvato. Restituisci il contenuto modificato, un risultato di errore hook sandbox oppure void per lasciarlo invariato.

Per rifiutare un salvataggio dalla sandbox, restituisci un risultato hook con versione contenente un errore SAVE_REJECTED. Imposta reason su testo semplice di 1–500 caratteri. EmDash identifica il plugin e mostra il motivo all’editor. I risultati di errore vuoti, troppo lunghi, malformati e sconosciuti fanno fallire il salvataggio con un errore hook generico.

"content:beforeSave": async (event, ctx) => {
	const { content } = event;
	if (typeof content.title !== "string" || content.title.trim() === "") {
		return {
			__emdashSandboxHookResult: true,
			version: 1,
			error: {
				code: "SAVE_REJECTED",
				reason: "Add a title before saving.",
			},
		};
	}

	content.title = content.title.trim();

	return content;
},

Non inserire HTML in reason. L’amministrazione renderizza il valore come testo.

Dal processo host, lancia invece ContentSaveRejectedError (esportato da emdash). L’API restituisce SAVE_REJECTED con il tuo messaggio. Qualsiasi altra eccezione, in entrambe le modalità di esecuzione, fa fallire il salvataggio con una risposta generica CONTENT_HOOK_ERROR.

Evento: { content, collection, isNew, id, actor } — Restituisce: contenuto modificato, un risultato di errore hook sandbox o void. In un aggiornamento, id è l’ID dell’elemento esistente e content contiene solo i valori dei campi inviati; carica l’elemento memorizzato con ctx.content.get(event.collection, event.id). Lo slug della voce non fa parte di content, e una chiave slug restituita dall’hook fallisce la validazione come campo sconosciuto. I salvataggi autenticati REST, di visual editing e MCP includono actor.id e il actor.role numerico. Le scritture interne senza utente autenticato omettono actor.

content:afterSave

Viene eseguito dopo che il contenuto è stato salvato con successo. Usalo per effetti collaterali come notifiche, logging o sincronizzazioni esterne.

"content:afterSave": async (event, ctx) => {
	const contentId = String(event.content.id);
	ctx.log.info(`${event.isNew ? "Created" : "Updated"} ${event.collection}/${contentId}`, {
		actorId: event.actor?.id,
	});

	if (ctx.http) {
		await ctx.http.fetch("https://api.example.com/webhook", {
			method: "POST",
			body: JSON.stringify({ event: "content:save", id: contentId }),
		});
	}
},

Evento: { content, collection, isNew, actor } — Restituisce: Promise<void>. I salvataggi autenticati includono lo stesso snapshot opzionale actor di content:beforeSave.

content:beforeDelete

Viene eseguito prima che il contenuto venga eliminato. Restituisci false per annullare; true o void lo consente.

"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, permanent: false } — Restituisce: boolean | void

Questo hook viene eseguito prima che una voce venga spostata nel cestino. La rimozione definitiva di una voce dal cestino non esegue di nuovo content:beforeDelete.

content:afterDelete

Viene eseguito dopo che il contenuto è stato eliminato con successo.

"content:afterDelete": async (event, ctx) => {
	await ctx.storage.cache.delete(`${event.collection}:${event.id}`);
},

Evento: { id, collection, permanent } — Restituisce: Promise<void>. permanent è false quando la voce è stata spostata nel cestino e true quando è stata rimossa permanentemente.

Dichiara hooks.content-policy:register per ispezionare e rifiutare pubblicazione, pianificazione o annullamento della pubblicazione senza ricevere accesso in lettura, scrittura o alle azioni di pubblicazione sul contenuto.

Restituisci void per consentire l’azione oppure { cancel: true, reason } per rifiutarla. Il motivo deve contenere 1–500 caratteri di testo semplice. Le decisioni non valide e gli errori imprevisti interrompono l’operazione per impostazione predefinita, senza esporre l’eccezione. I rifiuti espliciti restituiscono PUBLISH_REJECTED, SCHEDULE_REJECTED o UNPUBLISH_REJECTED.

Tutti e tre gli eventi contengono { content, collection, origin, actor? }. origin.source è api, mcp, visual-editor, plugin, scheduler o system; le origini plugin contengono anche pluginId. Le azioni umane autenticate includono actor.id, il actor.role numerico e il actor.source corrispondente. EmDash accetta l’origine visual-editor solo dal token di azione firmato e di breve durata incorporato in un rendering autenticato della barra degli strumenti; le normali richieste API non possono scegliere la propria origine.

Gli eventi di pubblicazione e pianificazione espongono la bozza effettiva in content.data e lo slug preparato in content.slug. Gli eventi di annullamento della pubblicazione espongono il contenuto attualmente online che l’azione rimuoverebbe.

content:beforePublish

L’hook seguente richiede un indicatore di approvazione prima che il contenuto possa andare online:

"content:beforePublish": async (event) => {
	const data = event.content.data;
	const approvalStatus =
		typeof data === "object" && data !== null && "approval_status" in data
			? data.approval_status
			: undefined;
	if (approvalStatus !== "approved") {
		return { cancel: true, reason: "Approve this entry before publishing." };
	}
},

Questo hook viene eseguito prima della pubblicazione manuale, MCP, da plugin, di sistema e pianificata. Il contenuto pianificato viene ricontrollato quando arriva l’orario di pubblicazione. Il rifiuto da parte dello scheduler annulla la pianificazione della voce, memorizza il motivo adatto alla visualizzazione pubblica ed elenca la voce interessata nella dashboard, invece di ritentare lo stesso rifiuto permanente a ogni tick dello scheduler. Una pianificazione, pubblicazione o eliminazione riuscita cancella il record. Un amministratore può chiudere un record obsoleto quando la voce o il plugin di policy non è più disponibile.

content:beforeSchedule

Viene eseguito prima che una voce riceva un orario di pubblicazione. L’evento contiene anche scheduledAt.

Non esiste un hook content:beforeUnschedule. Un amministratore può sempre annullare una pubblicazione futura.

content:beforeUnpublish

Viene eseguito prima che il contenuto online venga rimosso.

content:afterPublish

Viene eseguito dopo che il contenuto è stato promosso da bozza a online. Richiede la capability content:read.

Evento: { content, collection } — Restituisce: Promise<void>

content:afterUnpublish

Viene eseguito dopo che il contenuto è tornato da online a bozza. Richiede la capability content:read.

Evento: { content, collection } — Restituisce: Promise<void>

content:afterRestore

Viene eseguito dopo che il contenuto nel cestino è stato ripristinato. Richiede la capability content:read.

Evento: { content, collection } — Restituisce: Promise<void>

content:afterSchedule

Viene eseguito dopo che il contenuto è stato pianificato per una pubblicazione futura. Richiede la capability content:read.

Evento: { content, collection } — Restituisce: Promise<void>

content:afterUnschedule

Viene eseguito dopo che la pianificazione del contenuto pianificato è stata annullata. Richiede la capability content:read.

Evento: { content, collection } — Restituisce: Promise<void>

Hook media

media:beforeUpload

Viene eseguito prima che un file venga caricato. Restituisci i metadati del file modificati oppure 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 che un file è stato caricato con successo.

Evento: { media: { id, filename, mimeType, size, url, createdAt } } — Restituisce: Promise<void>

Hook di pagine pubbliche

Permettono ai plugin di contribuire alle pagine pubbliche renderizzate. I template li attivano includendo i componenti <EmDashHead>, <EmDashBodyStart> e <EmDashBodyEnd> da emdash/ui.

page:metadata

Aggiunge metadati tipizzati a <head>: meta tag, proprietà OpenGraph, <link> rel presenti in una allowlist e JSON-LD. Disponibile sia per i plugin sandboxed sia per quelli nativi. Il core convalida, 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 };
		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;
	}
}

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="<allowed value>" href="...">canonical: singleton; alternate: key o hreflang
jsonld<script type="application/ld+json">id (se presente)

Per qualsiasi chiave di deduplicazione vince il primo contributo. <EmDashHead> compone i contributi nell’ordine plugin → impostazioni del sito → metadati di base forniti dal template, quindi i contributi dei plugin prevalgono su tutto ciò che sta sotto. Nelle pagine di contenuto, i valori del pannello SEO della voce vengono incorporati nel contesto di pagina prima che vengano generati i metadati di base: sostituiscono i campi forniti dal template (e sono ciò che il tuo hook vede nel contesto di pagina), mentre i contributi dei plugin continuano a prevalere grazie alla deduplicazione «vince il primo». Il rel dei link è limitato a una allowlist bloccata per motivi di sicurezza (canonical, alternate, author, license, nlweb, site.standard.document); href deve essere HTTP o HTTPS.

page:fragments

Aggiunge 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 di prima parte (first-party) nel browser del visitatore, fuori da qualsiasi confine della sandbox. Per contributi alla pagina sicuri per la sandbox, usa page:metadata. Consulta Plugin nativi: frammenti di pagina se ti serve questa superficie.

Ordine di esecuzione degli hook

Quando un plugin in formato sandboxed viene eseguito in-process, gli hook usano la pipeline di hook condivisa:

  1. Gli hook con valori di priority più bassi vengono eseguiti per primi.
  2. A parità di priorità, gli hook vengono eseguiti nell’ordine di registrazione dei 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"],   // waits for A even if its priority would normally be later
	handler: async () => {},
}

Un runner di sandbox isolato invoca i plugin sandboxed attivi in ordine di caricamento. Mantieni gli hook indipendenti: non pretendere che un plugin sandboxed venga eseguito prima di un altro.

Gestione degli errori

I fallimenti degli hook sandboxed dipendono dal momento in cui l’hook viene eseguito:

  • Un errore lanciato in content:beforeSave fa fallire il salvataggio con CONTENT_HOOK_ERROR. Restituisci la busta SAVE_REJECTED documentata quando l’editor deve vedere un motivo di validazione specifico.
  • Restituire false da content:beforeDelete interrompe lo spostamento nel cestino. Se quell’hook lancia un’eccezione, EmDash registra l’errore e prosegue con l’eliminazione.
  • Gli hook after del contenuto vengono eseguiti dopo che l’operazione è riuscita. I loro errori vengono registrati e non possono annullare l’operazione.
  • Gli hook di ciclo di vita, media, e-mail e commenti seguono il contratto dell’operazione da cui hanno origine. Usa il Riferimento agli hook per verificare un valore di ritorno specifico prima di fare affidamento sul comportamento in caso di errore.

Un plugin in-process può usare errorPolicy: "abort" o "continue" nella forma di configurazione completa. Questa impostazione non è un controllo di ripristino portabile per un plugin sandboxed isolato.

Timeout

La pipeline di hook in-process ha come valore predefinito 5.000 ms e accetta un timeout più lungo nella forma di configurazione completa:

"content:afterSave": {
	timeout: 30000,
	handler: async (event, ctx) => {
		// Long-running operation
	},
},

Riferimento agli hook

HookAttivazioneRitornoEsclusivo
plugin:installPrima installazione del pluginvoidNo
plugin:activatePlugin abilitatovoidNo
plugin:deactivatePlugin disabilitatovoidNo
plugin:uninstallPlugin rimossovoidNo
content:beforeSavePrima del salvataggio del contenutoContenuto modificato, busta di rifiuto o voidNo
content:afterSaveDopo il salvataggio del contenutovoidNo
content:beforeDeletePrima che il contenuto venga spostato nel cestinofalse per annullare, altrimenti consentiNo
content:afterDeleteDopo lo spostamento nel cestino o l’eliminazione definitivavoidNo
content:afterPublishDopo la pubblicazione del contenutovoidNo
content:afterUnpublishDopo l’annullamento della pubblicazionevoidNo
content:afterRestoreDopo il ripristino del contenutovoidNo
content:afterScheduleDopo la pianificazione del contenutovoidNo
content:afterUnscheduleDopo l’annullamento della pianificazionevoidNo
media:beforeUploadPrima del caricamento di un fileInformazioni sul file modificate o voidNo
media:afterUploadDopo il caricamento di un filevoidNo
cronSi attiva un’attività pianificatavoidNo
email:beforeSendPrima della consegna dell’e-mailMessaggio modificato, false o voidNo
email:deliverConsegna dell’e-mail tramite trasportovoidSì
email:afterSendDopo la consegna dell’e-mailvoidNo
comment:beforeCreatePrima che il commento venga memorizzatoEvento modificato, false o voidNo
comment:moderateDecidi lo stato del commento{ status, reason? }Sì
comment:afterCreateDopo la memorizzazione del commentovoidNo
comment:afterModerateL’amministratore cambia lo stato del commentovoidNo
byline:afterSaveDopo il salvataggio della bylinevoidNo
byline:afterDeleteDopo l’eliminazione della bylinevoidNo
page:metadataRendering della paginaContributi o nullNo
page:fragmentsRendering della pagina (solo nativo)Contributi o nullNo

Consulta il Riferimento agli hook per i tipi di evento completi e le firme degli handler.