Hooks

Auf dieser Seite

Mit Hooks können Plugins Code als Reaktion auf Ereignisse ausführen. Alle Hooks erhalten ein Ereignisobjekt und den Plugin-Kontext, und sie werden zum Zeitpunkt der Plugin-Definition deklariert – es gibt keine dynamische Registrierung zur Laufzeit.

Diese Seite behandelt sandboxed Plugins. Native Plugins verwenden dieselben Hook-Namen und Ereignistypen, nutzen aber die In-Process-Hook-Pipeline und können zusätzlich page:fragments registrieren. Das Ablehnen von Speichervorgängen in der Sandbox und das Fehlerverhalten isolierter Runner werden weiter unten beschrieben.

Hook-Signatur

Jeder Hook-Handler nimmt zwei Argumente entgegen:

async (event, ctx) => ReturnType;
  • event — Daten darüber, was gerade passiert ist (gespeicherter Inhalt, hochgeladene Medien, Lebenszyklusübergang usw.)
  • ctx — der PluginContext mit Speicher, KV, Logging und durch Capabilities abgesicherten APIs

Wenn Sie die Definition einer Konstante vom Typ SandboxedPlugin zuweisen, wird event aus dem Hook-Namen abgeleitet (der vollständige kanonische Ereignistyp) und ctx als PluginContext, sodass Handler keine Parameter-Annotationen benötigen. Exportieren Sie diese Konstante als Default-Export. Um einen Ereignistyp in einer Hilfsfunktion namentlich zu referenzieren, importieren Sie ihn aus emdash/plugin.

Hook-Konfiguration

Ein Hook kann als reiner Handler deklariert oder in ein Konfigurationsobjekt eingewickelt werden. Bevorzugen Sie die reine Form, es sei denn, das Plugin unterstützt zusätzlich gezielt die Ausführung im selben Prozess und benötigt die unten beschriebenen Metadaten.

Einfach

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

Vollständige Konfiguration

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

Konfigurationsoptionen

OptionTypStandardBeschreibung
prioritynumber100Ausführungsreihenfolge. Niedrigere Zahlen werden zuerst ausgeführt.
timeoutnumber5000Maximale Ausführungszeit in Millisekunden.
exclusivebooleanfalseNur ein Plugin kann der aktive Anbieter sein. Wird für email:deliver und comment:moderate verwendet.
handlerfunction—Die Hook-Handler-Funktion. Erforderlich.

Erforderliche Capabilities

Mehrere Hooks legen geschützte Daten offen oder können einen Vorgang verändern. EmDash registriert sie nur, wenn das Manifest die passende Capability deklariert:

HooksCapabilityGrund
content:beforeSavecontent:writeDer Hook kann übermittelte Inhalte ersetzen.
content:beforePublish, content:beforeSchedule, content:beforeUnpublishhooks.content-policy:registerDie Hooks können Änderungen am Veröffentlichungsstatus ablehnen.
Andere content:*-Hookscontent:readIhre Ereignisse legen Inhalte offen oder identifizieren einen Eintrag.
media:beforeUploadmedia:writeDer Hook kann Upload-Metadaten ersetzen oder den Upload stoppen.
media:afterUploadmedia:readSein Ereignis legt das gespeicherte Medienelement offen.
email:beforeSend, email:afterSendhooks.email-events:registerDie Hooks prüfen Ereignisse im Lebenszyklus von E-Mails.
email:deliverhooks.email-transport:registerDer Hook wird zum Anbieter des E-Mail-Transports.
Alle comment:*-Hooksusers:readKommentarereignisse können Kontaktinformationen der Autoren und Anfragemetadaten enthalten.
page:fragmentshooks.page-fragments:registerDer Hook fügt Seiteninhalte erster Hand ein und ist nur nativ verfügbar.

Lebenszyklus-Hooks, cron und page:metadata haben keine Registrierungs-Capability. Deklarieren Sie die aufgeführte Capability auch dann, wenn ein Hook nur sein Ereignis liest und die passende ctx-API nicht aufruft. Die Deklaration gibt dem Betreiber eine zutreffende Einwilligungsabfrage, sichert die ctx-API ab und ist erforderlich, wenn das Plugin im selben Prozess läuft. Capabilities und Sicherheit erklärt die Auswirkung zur Laufzeit.

Lebenszyklus-Hooks

Werden bei Installation, Aktivierung, Deaktivierung und Entfernung des Plugins ausgeführt.

plugin:install

Wird einmalig ausgeführt, wenn das Plugin zum ersten Mal zu einer Site hinzugefügt wird.

Dieses Beispiel setzt voraus, dass das Manifest eine items-Storage-Collection deklariert:

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

Ereignis: {} — Rückgabe: Promise<void>

plugin:activate

Wird ausgeführt, wenn das Plugin aktiviert wird (nach der Installation oder bei erneuter Aktivierung).

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

Ereignis: {} — Rückgabe: Promise<void>

plugin:deactivate

Wird ausgeführt, wenn das Plugin deaktiviert (aber nicht entfernt) wird.

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

Ereignis: {} — Rückgabe: Promise<void>

plugin:uninstall

Wird ausgeführt, wenn das Plugin von einer Site entfernt wird.

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

Ereignis: { deleteData: boolean } — Rückgabe: Promise<void>

Inhalts-Hooks

Werden bei Erstellungs-, Aktualisierungs- und Löschvorgängen an Site-Inhalten ausgeführt.

content:beforeSave

Wird ausgeführt, bevor Inhalte gespeichert werden. Geben Sie geänderte Inhalte, ein Sandbox-Hook-Fehlerergebnis oder void zurück, um sie unverändert zu lassen.

Um einen Speichervorgang aus der Sandbox heraus abzulehnen, geben Sie ein versioniertes Hook-Ergebnis mit einem SAVE_REJECTED-Fehler zurück. Setzen Sie reason auf reinen Text mit 1 bis 500 Zeichen. EmDash identifiziert das Plugin und zeigt dem Redakteur den Grund an. Leere, zu lange, fehlerhafte und unbekannte Fehlerergebnisse lassen den Speichervorgang mit einem generischen Hook-Fehler scheitern.

"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;
},

Fügen Sie kein HTML in reason ein. Der Admin rendert den Wert als Text.

Im Host-Prozess werfen Sie stattdessen ContentSaveRejectedError (exportiert aus emdash). Die API gibt SAVE_REJECTED mit Ihrer Nachricht zurück. Jede andere Ausnahme lässt den Speichervorgang in beiden Ausführungsmodi mit einer generischen CONTENT_HOOK_ERROR-Antwort scheitern.

Ereignis: { content, collection, isNew, id, actor } — Rückgabe: geänderte Inhalte, ein Sandbox-Hook-Fehlerergebnis oder void. Bei einer Aktualisierung ist id die ID des vorhandenen Elements, und content enthält nur die übermittelten Feldwerte; laden Sie das gespeicherte Element mit ctx.content.get(event.collection, event.id). Der Slug des Eintrags ist nicht Teil von content, und ein vom Hook zurückgegebener slug-Schlüssel scheitert an der Validierung als unbekanntes Feld. Authentifizierte REST-, Visual-Editing- und MCP-Speichervorgänge enthalten actor.id und die numerische actor.role. Interne Schreibvorgänge ohne authentifizierten Benutzer lassen actor weg.

content:afterSave

Wird ausgeführt, nachdem Inhalte erfolgreich gespeichert wurden. Verwenden Sie ihn für Nebeneffekte wie Benachrichtigungen, Logging oder externe Synchronisierungen.

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

Ereignis: { content, collection, isNew, actor } — Rückgabe: Promise<void>. Authentifizierte Speichervorgänge enthalten denselben optionalen actor-Snapshot wie content:beforeSave.

content:beforeDelete

Wird ausgeführt, bevor Inhalte gelöscht werden. Geben Sie false zurück, um abzubrechen; true oder void erlaubt das Löschen.

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

Ereignis: { id, collection, permanent: false } — Rückgabe: boolean | void

Dieser Hook wird ausgeführt, bevor ein Eintrag in den Papierkorb verschoben wird. Wird ein Eintrag dauerhaft aus dem Papierkorb entfernt, läuft content:beforeDelete nicht erneut.

content:afterDelete

Wird ausgeführt, nachdem Inhalte erfolgreich gelöscht wurden.

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

Ereignis: { id, collection, permanent } — Rückgabe: Promise<void>. permanent ist false, wenn der Eintrag in den Papierkorb verschoben wurde, und true, wenn der Eintrag dauerhaft entfernt wurde.

Deklarieren Sie hooks.content-policy:register, um Veröffentlichung, Planung oder Rücknahme der Veröffentlichung zu prüfen und abzulehnen, ohne Lese-, Schreib- oder Veröffentlichungszugriff auf Inhalte zu erhalten.

Geben Sie void zurück, um die Aktion zu erlauben, oder { cancel: true, reason }, um sie abzulehnen. Der Grund muss 1–500 Zeichen reinen Text enthalten. Ungültige Entscheidungen und unerwartete Fehler brechen standardmäßig ab, ohne die Ausnahme offenzulegen. Explizite Ablehnungen geben PUBLISH_REJECTED, SCHEDULE_REJECTED oder UNPUBLISH_REJECTED zurück.

Alle drei Ereignisse enthalten { content, collection, origin, actor? }. origin.source ist api, mcp, visual-editor, plugin, scheduler oder system; Plugin-Origins enthalten außerdem pluginId. Authentifizierte menschliche Aktionen enthalten actor.id, die numerische actor.role und die passende actor.source. EmDash akzeptiert den visual-editor-Origin nur über das signierte, kurzlebige Aktions-Token, das in einem authentifizierten Toolbar-Rendering eingebettet ist; gewöhnliche API-Anfragen können ihren Origin nicht wählen.

Veröffentlichungs- und Planungsereignisse legen den wirksamen Entwurf in content.data und den vorbereiteten Slug in content.slug offen. Ereignisse zur Rücknahme der Veröffentlichung legen die aktuell live geschalteten Inhalte offen, die die Aktion entfernen würde.

content:beforePublish

Der folgende Hook verlangt eine Freigabemarkierung, bevor Inhalte live gehen können:

"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." };
	}
},

Dieser Hook wird vor der manuellen, der MCP-, der Plugin-, der System- und der geplanten Veröffentlichung ausgeführt. Geplante Inhalte werden erneut geprüft, wenn ihr Veröffentlichungszeitpunkt eintritt. Eine Ablehnung durch den Scheduler hebt die Planung des Eintrags auf, speichert den öffentlich unbedenklichen Grund und listet den betroffenen Eintrag im Dashboard auf, statt dieselbe dauerhafte Ablehnung bei jedem Scheduler-Tick erneut zu versuchen. Eine erfolgreiche Planung, Veröffentlichung oder Löschung löscht den Datensatz. Ein Administrator kann einen veralteten Datensatz verwerfen, wenn der Eintrag oder das Richtlinien-Plugin nicht mehr verfügbar ist.

content:beforeSchedule

Wird ausgeführt, bevor ein Eintrag einen Veröffentlichungszeitpunkt erhält. Das Ereignis enthält außerdem scheduledAt.

Es gibt keinen content:beforeUnschedule-Hook. Ein Administrator kann eine zukünftige Veröffentlichung immer abbrechen.

content:beforeUnpublish

Wird ausgeführt, bevor live geschaltete Inhalte entfernt werden.

content:afterPublish

Wird ausgeführt, nachdem Inhalte vom Entwurf zu live befördert wurden. Erfordert die Capability content:read.

Ereignis: { content, collection } — Rückgabe: Promise<void>

content:afterUnpublish

Wird ausgeführt, nachdem Inhalte von live wieder zum Entwurf zurückgesetzt wurden. Erfordert die Capability content:read.

Ereignis: { content, collection } — Rückgabe: Promise<void>

content:afterRestore

Wird ausgeführt, nachdem Inhalte aus dem Papierkorb wiederhergestellt wurden. Erfordert die Capability content:read.

Ereignis: { content, collection } — Rückgabe: Promise<void>

content:afterSchedule

Wird ausgeführt, nachdem Inhalte für eine zukünftige Veröffentlichung geplant wurden. Erfordert die Capability content:read.

Ereignis: { content, collection } — Rückgabe: Promise<void>

content:afterUnschedule

Wird ausgeführt, nachdem die Planung geplanter Inhalte aufgehoben wurde. Erfordert die Capability content:read.

Ereignis: { content, collection } — Rückgabe: Promise<void>

Medien-Hooks

media:beforeUpload

Wird ausgeführt, bevor eine Datei hochgeladen wird. Geben Sie geänderte Datei-Metadaten zurück oder werfen Sie eine Ausnahme, um abzubrechen.

"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}` };
},

Ereignis: { file: { name, type, size } } — Rückgabe: geänderte Datei oder void

media:afterUpload

Wird ausgeführt, nachdem eine Datei erfolgreich hochgeladen wurde.

Ereignis: { media: { id, filename, mimeType, size, url, createdAt } } — Rückgabe: Promise<void>

Öffentliche Seiten-Hooks

Damit können Plugins zu gerenderten öffentlichen Seiten beitragen. Templates aktivieren sie, indem sie die Komponenten <EmDashHead>, <EmDashBodyStart> und <EmDashBodyEnd> aus emdash/ui einbinden.

page:metadata

Liefert typisierte Metadaten für <head> – Meta-Tags, OpenGraph-Eigenschaften, <link>-Rels aus einer Allowlist und JSON-LD. Verfügbar für sandboxed und native Plugins. Der Core validiert, dedupliziert und rendert die Beiträge; Plugins geben strukturierte Daten zurück, niemals rohes HTML.

"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,
		},
	};
},

Ereignis:

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

Rückgabe: PageMetadataContribution | PageMetadataContribution[] | null

Beitragsarten:

ArtRendertDeduplizierungsschlüssel
meta<meta name="..." content="...">key oder name
property<meta property="..." content="...">key oder property
link<link rel="<allowed value>" href="...">canonical: Singleton; alternate: key oder hreflang
jsonld<script type="application/ld+json">id (falls vorhanden)

Bei jedem Deduplizierungsschlüssel gewinnt der erste Beitrag. <EmDashHead> setzt die Beiträge in der Reihenfolge Plugins → Site-Einstellungen → vom Template bereitgestellte Basis-Metadaten zusammen, sodass Plugin-Beiträge alles darunter überschreiben. Auf Inhaltsseiten werden die Werte aus dem SEO-Panel des Eintrags in den Seitenkontext übernommen, bevor die Basis-Metadaten erzeugt werden – sie ersetzen die vom Template bereitgestellten Felder (und sind das, was Ihr Hook im Seitenkontext sieht), während Plugin-Beiträge durch die Deduplizierung nach dem Prinzip „der Erste gewinnt“ weiterhin Vorrang haben. Das rel eines Links ist auf eine sicherheitsgesperrte Allowlist beschränkt (canonical, alternate, author, license, nlweb, site.standard.document); href muss HTTP oder HTTPS sein.

page:fragments

Liefert rohes HTML, Skripte oder Stylesheets für Einfügepunkte auf der Seite. Nur native Plugins.

Sandboxed Plugins können diesen Hook nicht verwenden, weil seine Ausgabe als Code erster Hand im Browser des Besuchers läuft, außerhalb jeder Sandbox-Grenze. Verwenden Sie für sandbox-sichere Seitenbeiträge page:metadata. Siehe Native Plugins: Seitenfragmente, wenn Sie diese Möglichkeit benötigen.

Hook-Ausführungsreihenfolge

Wenn ein Plugin im Sandbox-Format im selben Prozess läuft, verwenden Hooks die gemeinsame Hook-Pipeline:

  1. Hooks mit niedrigeren priority-Werten werden zuerst ausgeführt.
  2. Bei gleicher Priorität werden Hooks in der Reihenfolge der Plugin-Registrierung ausgeführt.
  3. Hooks mit dependencies warten, bis diese Plugins abgeschlossen sind.
// 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 () => {},
}

Ein isolierter Sandbox-Runner ruft aktive sandboxed Plugins in der Ladereihenfolge auf. Halten Sie Hooks unabhängig voneinander: Verlangen Sie nicht, dass ein sandboxed Plugin vor einem anderen läuft.

Fehlerbehandlung

Fehler in sandboxed Hooks hängen davon ab, wann der Hook ausgeführt wird:

  • Ein geworfener Fehler in content:beforeSave lässt den Speichervorgang mit CONTENT_HOOK_ERROR scheitern. Geben Sie den dokumentierten SAVE_REJECTED-Umschlag zurück, wenn der Redakteur einen konkreten Validierungsgrund sehen soll.
  • Wenn content:beforeDelete false zurückgibt, wird das Verschieben in den Papierkorb gestoppt. Wirft dieser Hook eine Ausnahme, protokolliert EmDash den Fehler und setzt das Löschen fort.
  • Inhalts-After-Hooks laufen, nachdem der Vorgang erfolgreich war. Ihre Fehler werden protokolliert und können den Vorgang nicht zurückrollen.
  • Lebenszyklus-, Medien-, E-Mail- und Kommentar-Hooks folgen dem Vertrag des Vorgangs, aus dem sie stammen. Prüfen Sie mit der Hook-Referenz einen bestimmten Rückgabewert, bevor Sie sich auf das Fehlerverhalten verlassen.

Ein In-Process-Plugin kann in der vollständigen Konfigurationsform errorPolicy: "abort" oder "continue" verwenden. Diese Einstellung ist keine portable Wiederherstellungskontrolle für ein isoliertes sandboxed Plugin.

Timeouts

Die In-Process-Hook-Pipeline verwendet standardmäßig 5.000 ms und akzeptiert in der vollständigen Konfigurationsform ein längeres timeout:

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

Hook-Referenz

HookAuslöserRückgabeExklusiv
plugin:installErstinstallation des PluginsvoidNein
plugin:activatePlugin aktiviertvoidNein
plugin:deactivatePlugin deaktiviertvoidNein
plugin:uninstallPlugin entferntvoidNein
content:beforeSaveVor dem Speichern von InhaltenGeänderte Inhalte, Ablehnungs-Umschlag oder voidNein
content:afterSaveNach dem Speichern von InhaltenvoidNein
content:beforeDeleteBevor Inhalte in den Papierkorb verschoben werdenfalse zum Abbrechen, sonst erlaubenNein
content:afterDeleteNach Papierkorb oder dauerhaftem LöschenvoidNein
content:afterPublishNach dem Veröffentlichen von InhaltenvoidNein
content:afterUnpublishNach der Rücknahme der VeröffentlichungvoidNein
content:afterRestoreNach dem Wiederherstellen von InhaltenvoidNein
content:afterScheduleNach dem Planen von InhaltenvoidNein
content:afterUnscheduleNach dem Aufheben der PlanungvoidNein
media:beforeUploadVor dem Datei-UploadGeänderte Dateiinformationen oder voidNein
media:afterUploadNach dem Datei-UploadvoidNein
cronGeplante Aufgabe wird ausgelöstvoidNein
email:beforeSendVor der E-Mail-ZustellungGeänderte Nachricht, false oder voidNein
email:deliverE-Mail über Transport zustellenvoidJa
email:afterSendNach der E-Mail-ZustellungvoidNein
comment:beforeCreateBevor ein Kommentar gespeichert wirdGeändertes Ereignis, false oder voidNein
comment:moderateKommentarstatus festlegen{ status, reason? }Ja
comment:afterCreateNach dem Speichern eines KommentarsvoidNein
comment:afterModerateAdmin ändert KommentarstatusvoidNein
byline:afterSaveNach dem Speichern einer BylinevoidNein
byline:afterDeleteNach dem Löschen einer BylinevoidNein
page:metadataSeiten-RenderingBeiträge oder nullNein
page:fragmentsSeiten-Rendering (nur nativ)Beiträge oder nullNein

Vollständige Ereignistypen und Handler-Signaturen finden Sie in der Hook-Referenz.