Hooks ermöglichen es Plugins, Code als Reaktion auf Ereignisse auszuführen. Alle Hooks erhalten ein Event-Objekt und den Plugin-Kontext, und sie werden zur Plugin-Definitionszeit deklariert – es gibt keine dynamische Registrierung zur Laufzeit.
Diese Seite behandelt Sandbox-Plugins. Hooks funktionieren in nativen Plugins identisch; native Plugins können zusätzlich page:fragments registrieren.
Hook-Signatur
Jeder Hook-Handler nimmt zwei Argumente:
async (event, ctx) => ReturnType;
event— Daten über das gerade Geschehene (Inhalt wird gespeichert, Medien hochgeladen, Lebenszyklusübergang usw.)ctx— derPluginContextmit Storage, KV, Logging und fähigkeitsgesteuerten APIs
satisfies SandboxedPlugin beim Default-Export leitet event vom Hook-Namen ab (der vollständige kanonische Event-Typ) und ctx als PluginContext, sodass Handler keine Parameter-Annotationen benötigen. Um einen Event-Typ nach Namen in einem Helper zu referenzieren, importieren Sie ihn aus emdash/plugin.
Hook-Konfiguration
Ein Hook kann als reiner Handler deklariert oder in ein Konfigurationsobjekt eingebettet 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
| Option | Typ | Standard | Beschreibung |
|---|---|---|---|
priority | number | 100 | Ausführungsreihenfolge. Niedrigere Zahlen werden zuerst ausgeführt. |
timeout | number | 5000 | Maximale Ausführungszeit in Millisekunden. |
dependencies | string[] | [] | Plugin-IDs, die vor diesem Hook ausgeführt werden müssen. |
errorPolicy | "abort" | "continue" | "abort" | Ob die Pipeline bei einem Fehler gestoppt werden soll. |
exclusive | boolean | false | Nur ein Plugin kann der aktive Anbieter sein. Wird für email:deliver und comment:moderate verwendet. |
handler | function | — | Die 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 Site 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: {} — 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");
},
Event: {} — Rückgabe: 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: {} — 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) {
const result = await ctx.storage.items.query({ limit: 1000 });
await ctx.storage.items.deleteMany(result.items.map((i) => i.id));
}
},
Event: { deleteData: boolean } — Rückgabe: Promise<void>
Content-Hooks
Werden während Erstell-, Aktualisierungs- und Löschoperationen auf Site-Inhalten ausgeführt.
content:beforeSave
Wird ausgeführt, bevor Inhalt gespeichert wird. Geben Sie modifizierten Inhalt zurück oder void, um ihn unverändert zu lassen. Werfen Sie einen Fehler, 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 } — Rückgabe: modifizierter Inhalt oder void.
content:afterSave
Wird ausgeführt, nachdem Inhalt erfolgreich gespeichert wurde. Verwenden Sie dies für Nebeneffekte 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 } — Rückgabe: Promise<void>
content:beforeDelete
Wird ausgeführt, bevor Inhalt gelöscht wird. Geben Sie 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 } — Rückgabe: 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 } — Rückgabe: Promise<void>
content:afterPublish
Wird ausgeführt, nachdem Inhalt von Entwurf zu Live befördert wurde. Erfordert die Fähigkeit content:read.
Event: { content, collection } — Rückgabe: Promise<void>
content:afterUnpublish
Wird ausgeführt, nachdem Inhalt von Live zu Entwurf zurückgesetzt wurde. Erfordert die Fähigkeit content:read.
Event: { content, collection } — Rückgabe: Promise<void>
Medien-Hooks
media:beforeUpload
Wird ausgeführt, bevor eine Datei hochgeladen wird. Geben Sie modifizierte Datei-Metadaten zurück oder werfen Sie einen Fehler, 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 } } — Rückgabe: modifizierte Datei oder void
media:afterUpload
Wird ausgeführt, nachdem eine Datei erfolgreich hochgeladen wurde.
Event: { media: { id, filename, mimeType, size, url, createdAt } } — Rückgabe: Promise<void>
Öffentliche Seiten-Hooks
Diese ermöglichen es Plugins, zu gerenderten öffentlichen Seiten beizutragen. Templates opt-in durch Einbindung der Komponenten <EmDashHead>, <EmDashBodyStart> und <EmDashBodyEnd> aus emdash/ui.
page:metadata
Trägt typisierte Metadaten zu <head> bei — Meta-Tags, OpenGraph-Eigenschaften, zugelassene <link>-Rels und JSON-LD. Verfügbar für sowohl Sandbox- 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 };
}
}
Rückgabe: PageMetadataContribution | PageMetadataContribution[] | null
Beitragsarten:
| Art | Rendert | Dedupe-Schlü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 Dedupe-Schlüssel. Link-rel ist auf eine sicherheitsgesperrte Whitelist 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.
Sandbox-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. Verwenden Sie für sandbox-sichere Seiten-Beiträge page:metadata. Siehe Native Plugins: Seiten-Fragmente, wenn Sie diese Oberfläche benötigen.
Hook-Ausführungsreihenfolge
Hooks werden in dieser Reihenfolge ausgeführt:
- Hooks mit niedrigeren
priority-Werten werden zuerst ausgeführt. - Bei gleichen Prioritäten werden Hooks in Plugin-Registrierungsreihenfolge ausgeführt.
- Hooks mit
dependencieswarten darauf, dass 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 () => {},
}
Fehlerbehandlung
Wenn ein Hook einen Fehler wirft oder eine Zeitüberschreitung auftritt:
errorPolicy: "abort"— die gesamte Pipeline stoppt und die ursprüngliche Operation kann fehlschlagen.errorPolicy: "continue"— der Fehler wird protokolliert 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");
},
},
Zeitüberschreitungen
Hooks haben standardmäßig 5000ms. Erhöhen Sie die Zeitüberschreitung für langsamere Arbeiten:
"content:afterSave": {
timeout: 30000,
handler: async (event, ctx) => {
// Long-running operation
},
},
Hook-Referenz
| Hook | Auslöser | Rückgabe | Exklusiv |
|---|---|---|---|
plugin:install | Erste Plugin-Installation | void | Nein |
plugin:activate | Plugin aktiviert | void | Nein |
plugin:deactivate | Plugin deaktiviert | void | Nein |
plugin:uninstall | Plugin entfernt | void | Nein |
content:beforeSave | Vor Inhalts-Speicherung | Modifizierter Inhalt oder void | Nein |
content:afterSave | Nach Inhalts-Speicherung | void | Nein |
content:beforeDelete | Vor Inhalts-Löschung | false zum Abbrechen, sonst erlauben | Nein |
content:afterDelete | Nach Inhalts-Löschung | void | Nein |
content:afterPublish | Nach Inhalts-Veröffentlichung | void | Nein |
content:afterUnpublish | Nach Inhalts-Rücknahme | void | Nein |
media:beforeUpload | Vor Datei-Upload | Modifizierte Dateiinfo oder void | Nein |
media:afterUpload | Nach Datei-Upload | void | Nein |
cron | Geplante Aufgabe wird ausgelöst | void | Nein |
email:beforeSend | Vor E-Mail-Zustellung | Modifizierte Nachricht, false oder void | Nein |
email:deliver | E-Mail über Transport zustellen | void | Ja |
email:afterSend | Nach E-Mail-Zustellung | void | Nein |
comment:beforeCreate | Vor Kommentar-Speicherung | Modifiziertes Event, false oder void | Nein |
comment:moderate | Kommentar-Status entscheiden | { status, reason? } | Ja |
comment:afterCreate | Nach Kommentar-Speicherung | void | Nein |
comment:afterModerate | Admin ändert Kommentar-Status | void | Nein |
page:metadata | Seiten-Rendering | Beiträge oder null | Nein |
page:fragments | Seiten-Rendering (nur nativ) | Beiträge oder null | Nein |
Siehe die Hook-Referenz für vollständige Event-Typen und Handler-Signaturen.