Hooks

Auf dieser Seite

Hooks ermöglichen es Plugins, Code als Reaktion auf Ereignisse auszuführen. Alle Hooks erhalten ein Event-Objekt und den Plugin-Kontext und werden bei der Plugin-Definition deklariert — es gibt keine dynamische Registrierung zur Laufzeit.

Diese Seite behandelt Sandboxed-Plugins. Hooks funktionieren bei nativen Plugins identisch; native Plugins können zusätzlich page:fragments registrieren.

Hook-Signatur

Jeder Hook-Handler nimmt zwei Argumente entgegen:

async (event, ctx) => ReturnType;
  • event — Daten über das, was gerade passiert ist (Inhalt wird gespeichert, Medien hochgeladen, Lebenszyklus-Übergang usw.)
  • ctx — der PluginContext mit Storage, KV, Logging und fähigkeitsgesteuerten APIs

satisfies SandboxedPlugin am Default-Export leitet event vom Hook-Namen (dem vollständigen kanonischen Ereignistyp) und ctx als PluginContext ab, sodass Handler keine Parameter-Annotationen benötigen. Um einen Event-Typ namentlich in einem Helper zu referenzieren, importieren Sie ihn aus emdash/plugin.

Hook-Konfiguration

Ein Hook kann als einfacher Handler oder in einem Config-Objekt deklariert werden:

Einfach

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

Vollständige Konfiguration

hooks: {
	"content:afterSave": {
		priority: 100,
		timeout: 5000,
		dependencies: ["audit-log"],
		errorPolicy: "continue",
		handler: async (event, ctx) => {
			ctx.log.info("Content saved");
		},
	},
},

Konfigurationsoptionen

OptionTypStandardBeschreibung
prioritynumber100Ausführungsreihenfolge. Niedrigere Zahlen werden zuerst ausgeführt.
timeoutnumber5000Maximale Ausführungszeit in Millisekunden.
dependenciesstring[][]Plugin-IDs, die vor diesem Hook laufen müssen.
errorPolicy"abort" | "continue""abort"Ob die Pipeline bei einem Fehler gestoppt werden soll.
exclusivebooleanfalseNur ein Plugin kann der aktive Anbieter sein. Verwendet für email:deliver und comment:moderate.
handlerfunctionDie Hook-Handler-Funktion. Erforderlich.

Lebenszyklus-Hooks

Werden während der Plugin-Installation, Aktivierung, Deaktivierung und Entfernung ausgeführt.

plugin:install

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

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

Event: {}Gibt zurück: Promise<void>

plugin:activate

Wird ausgeführt, wenn das Plugin aktiviert wird (nach der Installation oder beim erneuten Aktivieren).

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

Event: {}Gibt zurück: Promise<void>

plugin:deactivate

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

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

Event: {}Gibt zurück: Promise<void>

plugin:uninstall

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

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

Event: { deleteData: boolean }Gibt zurück: Promise<void>

Content-Hooks

Werden bei Erstellen-, Aktualisieren- und Löschen-Operationen auf Seiteninhalte ausgeführt.

content:beforeSave

Wird ausgeführt, bevor Inhalt gespeichert wird. Gibt modifizierten Inhalt zurück, oder void, um ihn unverändert zu lassen. Wirft eine Exception, um abzubrechen.

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

Event: { content, collection, isNew }Gibt zurück: modifizierten Inhalt oder void.

content:afterSave

Wird ausgeführt, nachdem Inhalt erfolgreich gespeichert wurde. Verwenden Sie es für Seiteneffekte wie Benachrichtigungen, Logging oder externe Synchronisierungen.

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

Event: { content, collection, isNew }Gibt zurück: Promise<void>

content:beforeDelete

Wird ausgeführt, bevor Inhalt gelöscht wird. Gibt false zurück, um abzubrechen; true oder void erlaubt es.

"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 }Gibt zurück: boolean | void

content:afterDelete

Wird ausgeführt, nachdem Inhalt erfolgreich gelöscht wurde.

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

Event: { id, collection }Gibt zurück: Promise<void>

content:afterPublish

Wird ausgeführt, nachdem Inhalt von Entwurf auf Live befördert wurde. Erfordert die Fähigkeit content:read.

Event: { content, collection }Gibt zurück: Promise<void>

content:afterUnpublish

Wird ausgeführt, nachdem Inhalt von Live auf Entwurf zurückgesetzt wurde. Erfordert die Fähigkeit content:read.

Event: { content, collection }Gibt zurück: Promise<void>

content:afterRestore

Wird ausgeführt, nachdem gelöschter Inhalt wiederhergestellt wurde. Erfordert die Fähigkeit content:read.

Event: { content, collection }Gibt zurück: Promise<void>

content:afterSchedule

Wird ausgeführt, nachdem Inhalt für zukünftige Veröffentlichung geplant wurde. Erfordert die Fähigkeit content:read.

Event: { content, collection }Gibt zurück: Promise<void>

content:afterUnschedule

Wird ausgeführt, nachdem geplanter Inhalt aufgehoben wurde. Erfordert die Fähigkeit content:read.

Event: { content, collection }Gibt zurück: Promise<void>

Medien-Hooks

media:beforeUpload

Wird ausgeführt, bevor eine Datei hochgeladen wird. Gibt modifizierte Datei-Metadaten zurück oder wirft eine Exception, 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}` };
},

Event: { file: { name, type, size } }Gibt zurück: modifizierte Datei oder void

media:afterUpload

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

Event: { media: { id, filename, mimeType, size, url, createdAt } }Gibt zurück: Promise<void>

Public-Page-Hooks

Diese ermöglichen es Plugins, zu gerenderten öffentlichen Seiten beizutragen. Templates aktivieren dies durch Einbindung der Komponenten <EmDashHead>, <EmDashBodyStart> und <EmDashBodyEnd> aus emdash/ui.

page:metadata

Trägt typisierte Metadaten zu <head> bei — Meta-Tags, OpenGraph-Eigenschaften, erlaubte <link>-Rels und JSON-LD. Verfügbar für sowohl Sandboxed- als auch native Plugins. 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,
		},
	};
},

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

Gibt zurück: PageMetadataContribution | PageMetadataContribution[] | null

Beitragsarten:

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

Der erste Beitrag gewinnt für jeden Deduplizierungsschlüssel. Link rel ist auf eine sicherheitsgesperrte Erlaubnisliste beschränkt (canonical, alternate, author, license, nlweb, site.standard.document); href muss HTTP oder HTTPS sein.

page:fragments

Trägt rohes HTML, Skripte oder Stylesheets zu Seiten-Einfügepunkten bei. Nur native Plugins.

Sandboxed-Plugins können diesen Hook nicht verwenden, da seine Ausgabe als First-Party-Code im Browser des Besuchers läuft, außerhalb jeder Sandbox-Grenze. Für sandbox-sichere Seitenbeiträge verwenden Sie page:metadata. Siehe Native Plugins: Page Fragments, wenn Sie diese Oberfläche benötigen.

Hook-Ausführungsreihenfolge

Hooks werden in dieser Reihenfolge ausgeführt:

  1. Hooks mit niedrigeren priority-Werten werden zuerst ausgeführt.
  2. Bei gleichen Prioritäten werden Hooks in Plugin-Registrierungsreihenfolge ausgeführt.
  3. Hooks mit dependencies warten darauf, dass diese Plugins abgeschlossen werden.
// 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"],   // wartet auf A, selbst wenn seine Priorität normalerweise später wäre
	handler: async () => {},
}

Fehlerbehandlung

Wenn ein Hook eine Exception wirft oder ein Timeout hat:

  • errorPolicy: "abort" — die gesamte Pipeline stoppt und die auslösende Operation kann fehlschlagen.
  • errorPolicy: "continue" — der Fehler wird geloggt und die verbleibenden Hooks werden weiterhin ausgeführt.
"content:afterSave": {
	timeout: 5000,
	errorPolicy: "continue",
	handler: async (event, ctx) => {
		await ctx.http!.fetch("https://unreliable-api.com/notify");
	},
},

Timeouts

Hooks haben standardmäßig 5000ms. Erhöhen Sie das Timeout für langsamere Arbeiten:

"content:afterSave": {
	timeout: 30000,
	handler: async (event, ctx) => {
		// Langfristige Operation
	},
},

Hook-Referenz

HookAuslöserRückgabeExklusiv
plugin:installErste Plugin-InstallationvoidNein
plugin:activatePlugin aktiviertvoidNein
plugin:deactivatePlugin deaktiviertvoidNein
plugin:uninstallPlugin entferntvoidNein
content:beforeSaveVor dem SpeichernModifizierter Inhalt oder voidNein
content:afterSaveNach dem SpeichernvoidNein
content:beforeDeleteVor dem Löschenfalse zum Abbrechen, sonst erlaubtNein
content:afterDeleteNach dem LöschenvoidNein
content:afterPublishNach VeröffentlichungvoidNein
content:afterUnpublishNach RücknahmevoidNein
content:afterRestoreNach WiederherstellungvoidNein
content:afterScheduleNach PlanungvoidNein
content:afterUnscheduleNach Aufhebung der PlanungvoidNein
media:beforeUploadVor dem Datei-UploadModifizierte Dateiinfo oder voidNein
media:afterUploadNach dem Datei-UploadvoidNein
cronGeplante Aufgabe ausgelöstvoidNein
email:beforeSendVor E-Mail-ZustellungModifizierte Nachricht, false oder voidNein
email:deliverE-Mail über Transport zustellenvoidJa
email:afterSendNach E-Mail-ZustellungvoidNein
comment:beforeCreateVor Kommentar-SpeicherungModifiziertes Event, false oder voidNein
comment:moderateKommentar-Status entscheiden{ status, reason? }Ja
comment:afterCreateNach Kommentar-SpeicherungvoidNein
comment:afterModerateAdmin ändert Kommentar-StatusvoidNein
page:metadataSeiten-RenderingBeiträge oder nullNein
page:fragmentsSeiten-Rendering (nur nativ)Beiträge oder nullNein

Siehe die Hook-Referenz für vollständige Event-Typen und Handler-Signaturen.