Hook

In questa pagina

Gli hook consentono ai plugin di eseguire codice in risposta agli eventi. Tutti gli hook ricevono un oggetto evento e il contesto del plugin, e sono dichiarati al momento della definizione del plugin: non c’è registrazione dinamica a runtime.

Questa pagina riguarda i plugin sandbox. I plugin nativi usano gli stessi nomi di hook e tipi di evento, ma usano la pipeline di hook in-process e possono inoltre registrare page:fragments. Il rifiuto del salvataggio in sandbox e il comportamento di errore del runner isolato sono descritti sotto.

Firma dell’hook

Ogni gestore di hook prende due argomenti:

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

Assegnare la definizione a una costante tipizzata SandboxedPlugin inferisce event dal nome dell’hook (il tipo di evento canonico completo) e ctx come PluginContext, così i gestori non necessitano di annotazioni dei parametri. Esporta quella costante come default. Per riferire un tipo di evento per nome in un helper, importalo da emdash/plugin.

Configurazione dell’hook

Un hook può essere dichiarato come gestore semplice o avvolto in un oggetto di configurazione. Preferisci la forma semplice a meno che il plugin non supporti anche l’esecuzione in-process deliberata e non necessiti dei metadati descritti sotto.

Simple

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

Full config

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

Opzioni di configurazione

OptionTypeDefaultDescription
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 gestore 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:

HooksCapabilityReason
content:beforeSavecontent:writeL’hook può sostituire il contenuto inviato.
content:beforePublish, content:beforeSchedule, content:beforeUnpublishhooks.content-policy:registerGli hook possono rifiutare i cambiamenti di stato di pubblicazione.
Altri hook content:*content:readI loro eventi espongono contenuto o identificano una voce.
media:beforeUploadmedia:writeL’hook può sostituire i metadati di upload o interrompere l’upload.
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 dell’email.
email:deliverhooks.email-transport:registerL’hook diventa un provider di trasporto email.
Tutti gli hook comment:*users:readGli eventi di commento possono contenere informazioni di contatto dell’autore e metadati della richiesta.
page:fragmentshooks.page-fragments:registerL’hook inietta contenuto di pagina first-party ed è solo nativo.

Gli hook di ciclo di vita, cron e page:metadata non hanno capability di registrazione. Dichiara la capability elencata anche quando un hook legge solo il suo evento e non chiama l’API ctx corrispondente. La dichiarazione dà all’operatore un prompt di consenso accurato, controlla l’API ctx ed è richiesta quando il plugin gira in-process. Capability e sicurezza spiega l’effetto a runtime.

Hook di ciclo di vita

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

plugin:install

Viene eseguito una 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" });
},

Event: {} — Returns: Promise<void>

plugin:activate

Viene eseguito quando il plugin viene abilitato (dopo l’installazione o al riabilitamento).

"plugin:activate": async (_event, ctx) => {
	ctx.log.info("Plugin activated");
},

Event: {} — Returns: Promise<void>

plugin:deactivate

Viene eseguito quando il plugin viene disabilitato (ma non rimosso).

"plugin:deactivate": async (_event, ctx) => {
	ctx.log.info("Plugin deactivated");
},

Event: {} — Returns: 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));
		}
	}
},

Event: { deleteData: boolean } — Returns: Promise<void>

Hook di contenuto

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

content:beforeSave

Viene eseguito prima del salvataggio del contenuto. Restituisci contenuto modificato, un risultato di errore hook sandbox o void per lasciarlo invariato.

Per rifiutare un salvataggio dalla sandbox, restituisci un risultato hook versionato con un errore SAVE_REJECTED. Imposta reason su testo semplice tra 1 e 500 caratteri. EmDash identifica il plugin e mostra il motivo all’editor. 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.",
			},
		};
	}

	if (typeof content.slug === "string") {
		content.slug = content.slug.toLowerCase().replace(/\s+/g, "-");
	}

	return content;
},

Non mettere HTML in reason. L’admin rende 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 da entrambi i modi di esecuzione fa fallire il salvataggio con una risposta generica CONTENT_HOOK_ERROR.

Event: { content, collection, isNew, id, actor } — Returns: 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). 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 sync 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 }),
		});
	}
},

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

content:beforeDelete

Viene eseguito prima dell’eliminazione del contenuto. Restituisci false per annullare; true o void lo consentono.

"content:beforeDelete": async (event, ctx) => {
	if (event.collection === "pages" && event.id === "home") {
		ctx.log.warn("Cannot delete home page");
		return false;
	}
	return true;
},

Event: { id, collection, permanent: false } — Returns: boolean | void

Questo hook viene eseguito prima che una voce venga spostata nel cestino. La rimozione permanente di una voce dal cestino non riesegue 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}`);
},

Event: { id, collection, permanent } — Returns: 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 unpublish senza ricevere accesso di lettura, scrittura o azione di pubblicazione sul contenuto.

Restituisci void per consentire l’azione o { cancel: true, reason } per rifiutarla. Il motivo deve contenere 1–500 caratteri di testo semplice. Decisioni non valide ed errori imprevisti abortiscono 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, actor.role numerico e il corrispondente actor.source. EmDash accetta l’origine visual-editor solo dal token di azione firmato e di breve durata incorporato in un render della toolbar autenticato; le richieste API ordinarie non possono selezionare la propria origine.

Gli eventi di publish e schedule espongono la bozza effettiva in content.data e lo slug preparato in content.slug. Gli eventi di unpublish espongono il contenuto attualmente live che l’azione rimuoverebbe.

content:beforePublish

Il seguente hook richiede un marker di approvazione prima che il contenuto possa diventare live:

"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, plugin, di sistema e pianificata. Il contenuto pianificato viene controllato di nuovo quando arriva l’ora di pubblicazione. Un rifiuto dello scheduler annulla la pianificazione, memorizza il motivo sicuro per il pubblico e elenca la voce interessata nella dashboard invece di ritentare lo stesso rifiuto permanente a ogni tick dello scheduler. Una schedule, publish o delete riuscita cancella il record. Un amministratore può archiviare un record obsoleto quando la voce o il plugin di policy non è più disponibile.

content:beforeSchedule

Viene eseguito prima che una voce riceva un’ora 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 della rimozione del contenuto live.

content:afterPublish

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

Event: { content, collection } — Returns: Promise<void>

content:afterUnpublish

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

Event: { content, collection } — Returns: Promise<void>

content:afterRestore

Viene eseguito dopo il ripristino di contenuto dal cestino. Richiede la capability content:read.

Event: { content, collection } — Returns: Promise<void>

content:afterSchedule

Viene eseguito dopo la pianificazione del contenuto per pubblicazione futura. Richiede la capability content:read.

Event: { content, collection } — Returns: Promise<void>

content:afterUnschedule

Viene eseguito dopo l’annullamento della pianificazione del contenuto. Richiede la capability content:read.

Event: { content, collection } — Returns: Promise<void>

Hook media

media:beforeUpload

Viene eseguito prima del caricamento di un file. Restituisci metadati file modificati o lancia 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}` };
},

Event: { file: { name, type, size } } — Returns: file modificato o void

media:afterUpload

Viene eseguito dopo che un file è stato caricato con successo.

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

Hook di pagine pubbliche

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

page:metadata

Contribuisce metadati tipizzati a <head> — meta tag, proprietà OpenGraph, rel <link> in allowlist e JSON-LD. Disponibile per plugin sandbox e nativi. Il core convalida, deduplica e rende 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,
		},
	};
},

Event:

{
	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;
	}
}

Returns: PageMetadataContribution | PageMetadataContribution[] | null

Contribution kinds:

KindRendersDedupe key
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)

Il primo contributo vince per qualsiasi chiave di deduplicazione. <EmDashHead> compone i contributi nell’ordine plugin → impostazioni del sito → metadati base forniti dal template, così i contributi del plugin sovrascrivono tutto ciò che è sotto. Nelle pagine di contenuto, i valori del pannello SEO della voce vengono piegati nel contesto di pagina prima della generazione dei metadati base — sostituiscono i campi forniti dal template (e sono ciò che il tuo hook vede sul contesto di pagina), mentre i contributi del plugin vincono ancora tramite dedup first-wins. Il rel dei link è limitato a un’allowlist bloccata per 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 sandbox non possono usare questo hook perché il suo output gira come codice first-party nel browser del visitatore, fuori da qualsiasi confine di sandbox. Per contributi di pagina sicuri in sandbox, usa page:metadata. Vedi Plugin nativi: frammenti di pagina se ti serve questa superficie.

Ordine di esecuzione degli hook

Quando un plugin in formato sandbox gira in-process, gli hook usano la pipeline di hook condivisa:

  1. Gli hook con valori priority più bassi vengono eseguiti per primi.
  2. A priorità uguali, gli hook vengono eseguiti nell’ordine di registrazione dei plugin.
  3. Gli hook con dependencies attendono che quei plugin completino.
// 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 sandbox isolato invoca i plugin sandbox attivi in ordine di caricamento. Mantieni gli hook indipendenti: non richiedere che un plugin sandbox venga eseguito prima di un altro.

Gestione degli errori

I fallimenti degli hook sandbox dipendono da quando l’hook viene eseguito:

  • Un errore lanciato in content:beforeSave fa fallire il salvataggio con CONTENT_HOOK_ERROR. Restituisci l’envelope SAVE_REJECTED documentato quando l’editor deve vedere un motivo di validazione specifico.
  • Restituire false da content:beforeDelete interrompe lo spostamento nel cestino. Se quell’hook lancia, EmDash registra l’errore e continua l’eliminazione.
  • Gli after-hook di contenuto vengono eseguiti dopo il successo dell’operazione. I loro errori vengono registrati e non possono annullare l’operazione.
  • Gli hook di ciclo di vita, media, email e commenti seguono il contratto della loro operazione di origine. Usa il riferimento agli hook per controllare un valore di ritorno specifico prima di fare affidamento sul comportamento di errore.

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

Timeout

La pipeline di hook in-process ha un valore predefinito di 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

HookTriggerReturnExclusive
plugin:installPrima installazione del pluginvoidNo
plugin:activatePlugin abilitatovoidNo
plugin:deactivatePlugin disabilitatovoidNo
plugin:uninstallPlugin rimossovoidNo
content:beforeSavePrima del salvataggio del contenutoContenuto modificato, envelope di rifiuto o voidNo
content:afterSaveDopo il salvataggio del contenutovoidNo
content:beforeDeletePrima dello spostamento nel cestinofalse per annullare, altrimenti consentireNo
content:afterDeleteDopo cestino o eliminazione permanentevoidNo
content:afterPublishDopo la pubblicazione del contenutovoidNo
content:afterUnpublishDopo l’unpublish del contenutovoidNo
content:afterRestoreDopo il ripristino del contenutovoidNo
content:afterScheduleDopo la pianificazione del contenutovoidNo
content:afterUnscheduleDopo l’annullamento della pianificazionevoidNo
media:beforeUploadPrima dell’upload del fileInfo file modificate o voidNo
media:afterUploadDopo l’upload del filevoidNo
cronSi attiva un’attività pianificatavoidNo
email:beforeSendPrima della consegna dell’emailMessaggio modificato, false o voidNo
email:deliverConsegnare l’email via trasportovoidYes
email:afterSendDopo la consegna dell’emailvoidNo
comment:beforeCreatePrima della memorizzazione del commentoEvento modificato, false o voidNo
comment:moderateDecidere lo stato del commento{ status, reason? }Yes
comment:afterCreateDopo la memorizzazione del commentovoidNo
comment:afterModerateL’admin cambia lo stato del commentovoidNo
page:metadataRender di paginaContributi o nullNo
page:fragmentsRender di pagina (solo nativo)Contributi o nullNo

Vedi il riferimento agli hook per tipi di evento completi e firme dei gestori.