Zum Inhalt springen

Plugin-Hooks

Hooks ermöglichen es Plugins, Code als Reaktion auf Ereignisse auszuführen. Alle Hooks erhalten ein Event-Objekt und den Plugin-Kontext. Hooks werden zur Definitionszeit des Plugins deklariert, nicht dynamisch zur Laufzeit registriert.

Jeder Hook-Handler erhält zwei Argumente:

async (event: EventType, ctx: PluginContext) => ReturnType;
  • event — Daten zum Ereignis (gespeicherter Inhalt, hochgeladene Medien, etc.)
  • ctx — Der Plugin-Kontext mit Storage, KV, Logging und capability-gated APIs

Hooks können als einfacher Handler oder mit vollständiger Konfiguration deklariert werden:

hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved");
}
}
OptionTypStandardBeschreibung
prioritynumber100Ausführungsreihenfolge. Niedrigere Zahlen werden zuerst ausgeführt.
timeoutnumber5000Maximale Ausführungszeit in Millisekunden.
dependenciesstring[][]Plugin-IDs, die vor diesem Hook ausgeführt werden müssen.
errorPolicy"abort" | "continue""abort"Ob die Pipeline bei einem Fehler gestoppt werden soll.
exclusivebooleanfalseNur ein Plugin kann der aktive Anbieter sein. Wird für email:deliver und comment:moderate verwendet.
handlerfunction—Die Hook-Handler-Funktion. Erforderlich.

Lifecycle-Hooks werden während der Installation, Aktivierung und Deaktivierung eines Plugins ausgeführt.

Wird einmal ausgeführt, wenn das Plugin erstmals zu einer Site hinzugefügt wird.

"plugin:install": async (_event, ctx) => {
ctx.log.info("Installing plugin...");
// Seed default data
await ctx.kv.set("settings:enabled", true);
await ctx.storage.items!.put("default", { name: "Default Item" });
}

Event: {}
Returns: Promise<void>

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

Event: {}
Returns: Promise<void>

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

"plugin:deactivate": async (_event, ctx) => {
ctx.log.info("Plugin deactivated");
// Release resources, pause background work
}

Event: {}
Returns: Promise<void>

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

"plugin:uninstall": async (event, ctx) => {
ctx.log.info("Uninstalling plugin...");
if (event.deleteData) {
// User opted to delete plugin data
const result = await ctx.storage.items!.query({ limit: 1000 });
await ctx.storage.items!.deleteMany(result.items.map(i => i.id));
}
}

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

Content-Hooks werden während Erstellungs-, Aktualisierungs- und Löschvorgängen ausgeführt.

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

"content:beforeSave": async (event, ctx) => {
const { content, collection, isNew } = event;
// Validate
if (collection === "posts" && !content.title) {
throw new Error("Posts require a title");
}
// Transform
if (content.slug) {
content.slug = content.slug.toLowerCase().replace(/\s+/g, "-");
}
return content;
}

Event:

{
content: Record<string, unknown>; // Content data being saved
collection: string; // Collection name
isNew: boolean; // True if creating, false if updating
}

Returns: Promise<Record<string, unknown> | void>

Wird ausgeführt, nachdem Inhalt erfolgreich gespeichert wurde. Wird für Nebeneffekte wie Benachrichtigungen, Logging oder Synchronisation mit externen Systemen verwendet.

"content:afterSave": async (event, ctx) => {
const { content, collection, isNew } = event;
ctx.log.info(`${isNew ? "Created" : "Updated"} ${collection}/${content.id}`);
// Trigger external sync
if (ctx.http) {
await ctx.http.fetch("https://api.example.com/webhook", {
method: "POST",
body: JSON.stringify({ event: "content:save", id: content.id })
});
}
}

Event:

{
content: Record<string, unknown>; // Saved content (includes id, timestamps)
collection: string;
isNew: boolean;
}

Returns: Promise<void>

Wird ausgeführt, bevor Inhalt gelöscht wird. Gib false zurück, um das Löschen abzubrechen, true oder void, um es zu erlauben.

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

Event:

{
id: string; // Content ID being deleted
collection: string;
}

Returns: Promise<boolean | void>

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

"content:afterDelete": async (event, ctx) => {
const { id, collection } = event;
ctx.log.info(`Deleted ${collection}/${id}`);
// Clean up related plugin data
await ctx.storage.cache!.delete(`${collection}:${id}`);
}

Event:

{
id: string;
collection: string;
}

Returns: Promise<void>

Media-Hooks werden während Datei-Uploads ausgeführt.

Wird ausgeführt, bevor eine Datei hochgeladen wird. Gib modifizierte Dateiinformationen oder void zurück, um sie unverändert zu lassen. Wirf eine Exception, um den Upload abzubrechen.

"media:beforeUpload": async (event, ctx) => {
const { file } = event;
// Validate file type
if (!file.type.startsWith("image/")) {
throw new Error("Only images are allowed");
}
// Validate file size (10MB max)
if (file.size > 10 * 1024 * 1024) {
throw new Error("File too large");
}
// Rename file
return {
...file,
name: `${Date.now()}-${file.name}`
};
}

Event:

{
file: {
name: string; // Original filename
type: string; // MIME type
size: number; // Size in bytes
}
}

Returns: Promise<{ name: string; type: string; size: number } | void>

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

"media:afterUpload": async (event, ctx) => {
const { media } = event;
ctx.log.info(`Uploaded ${media.filename}`, {
id: media.id,
size: media.size,
mimeType: media.mimeType
});
}

Event:

{
media: {
id: string;
filename: string;
mimeType: string;
size: number | null;
url: string;
createdAt: string;
}
}

Returns: Promise<void>

Hooks werden in dieser Reihenfolge ausgeführt:

  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 auf den Abschluss dieser Plugins
// Plugin A
"content:afterSave": {
priority: 50, // Runs first
handler: async () => {}
}
// Plugin B
"content:afterSave": {
priority: 100, // Runs second (default priority)
handler: async () => {}
}
// Plugin C
"content:afterSave": {
priority: 200,
dependencies: ["plugin-a"], // Runs after A, even if priority was lower
handler: async () => {}
}

Wenn ein Hook eine Exception wirft oder ein Timeout auftritt:

  • errorPolicy: "abort" — Die gesamte Pipeline wird gestoppt. Der ursprüngliche Vorgang kann fehlschlagen.
  • errorPolicy: "continue" — Der Fehler wird geloggt und die verbleibenden Hooks werden weiterhin ausgeführt.
"content:afterSave": {
timeout: 5000,
errorPolicy: "continue", // Don't fail the save if this hook fails
handler: async (event, ctx) => {
// External API call that might fail
await ctx.http!.fetch("https://unreliable-api.com/notify");
}
}

Hooks haben ein Standard-Timeout von 5000ms (5 Sekunden). Erhöhe es für Operationen, die länger dauern können:

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

Öffentliche Seiten-Hooks ermöglichen es Plugins, zum <head> und <body> gerenderter Seiten beizutragen. Templates melden sich dafür an, indem sie die Komponenten <EmDashHead>, <EmDashBodyStart> und <EmDashBodyEnd> aus emdash/ui verwenden.

Trägt typisierte Metadaten zu <head> bei — Meta-Tags, OpenGraph-Eigenschaften, kanonische/alternative Links und JSON-LD strukturierte Daten. Funktioniert sowohl im trusted- als auch im sandboxed-Modus.

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.title,
description: event.page.description,
},
};
}

Ereignis:

{
page: {
url: string;
path: string;
locale: string | null;
kind: "content" | "custom";
pageType: string;
title: 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

Beitragstypen:

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

Der erste Beitrag gewinnt für jeden Deduplizierungsschlüssel. Link-HREFs müssen HTTP oder HTTPS sein.

Trägt rohes HTML, Skripte oder Markup zu Seiten-Einfügepunkten bei. Nur für trusted Plugins — sandboxed Plugins können diesen Hook nicht verwenden.

"page:fragments": async (event, ctx) => {
return {
kind: "external-script",
placement: "head",
src: "https://www.googletagmanager.com/gtm.js?id=GTM-XXXXX",
async: true,
};
}

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

Platzierungen: "head", "body:start", "body:end". Templates, die eine Komponente für eine Platzierung weglassen, ignorieren Beiträge, die darauf abzielen, stillschweigend.

HookAuslöserRückgabeExklusiv
plugin:installErstmalige Plugin-InstallationvoidNein
plugin:activatePlugin aktiviertvoidNein
plugin:deactivatePlugin deaktiviertvoidNein
plugin:uninstallPlugin entferntvoidNein
content:beforeSaveVor dem Speichern von InhaltModifizierter Inhalt oder voidNein
content:afterSaveNach dem Speichern von InhaltvoidNein
content:beforeDeleteVor dem Löschen von Inhaltfalse zum Abbrechen, sonst erlaubenNein
content:afterDeleteNach dem Löschen von InhaltvoidNein
media:beforeUploadVor dem Datei-UploadModifizierte Dateiinfo oder voidNein
media:afterUploadNach dem Datei-UploadvoidNein
cronGeplante Aufgabe wird ausgelöstvoidNein
email:beforeSendVor dem E-Mail-VersandModifizierte Nachricht, false, oder voidNein
email:deliverE-Mail über Transport sendenvoidJa
email:afterSendNach dem E-Mail-VersandvoidNein
comment:beforeCreateVor dem Speichern eines KommentarsModifiziertes Ereignis, false, oder voidNein
comment:moderateKommentarstatus entscheiden{ status, reason? }Ja
comment:afterCreateNach dem Speichern eines KommentarsvoidNein
comment:afterModerateAdmin ändert KommentarstatusvoidNein
page:metadataSeiten-RenderingBeiträge oder nullNein
page:fragmentsSeiten-Rendering (trusted)Beiträge oder nullNein

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