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— derPluginContextmit 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
| Option | Typ | Standard | Beschreibung |
|---|---|---|---|
priority | number | 100 | Ausführungsreihenfolge. Niedrigere Zahlen werden zuerst ausgeführt. |
timeout | number | 5000 | Maximale Ausführungszeit in Millisekunden. |
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. |
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:
| Hooks | Capability | Grund |
|---|---|---|
content:beforeSave | content:write | Der Hook kann übermittelte Inhalte ersetzen. |
content:beforePublish, content:beforeSchedule, content:beforeUnpublish | hooks.content-policy:register | Die Hooks können Änderungen am Veröffentlichungsstatus ablehnen. |
Andere content:*-Hooks | content:read | Ihre Ereignisse legen Inhalte offen oder identifizieren einen Eintrag. |
media:beforeUpload | media:write | Der Hook kann Upload-Metadaten ersetzen oder den Upload stoppen. |
media:afterUpload | media:read | Sein Ereignis legt das gespeicherte Medienelement offen. |
email:beforeSend, email:afterSend | hooks.email-events:register | Die Hooks prüfen Ereignisse im Lebenszyklus von E-Mails. |
email:deliver | hooks.email-transport:register | Der Hook wird zum Anbieter des E-Mail-Transports. |
Alle comment:*-Hooks | users:read | Kommentarereignisse können Kontaktinformationen der Autoren und Anfragemetadaten enthalten. |
page:fragments | hooks.page-fragments:register | Der 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:
| Art | Rendert | Deduplizierungsschlü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:
- Hooks mit niedrigeren
priority-Werten werden zuerst ausgeführt. - Bei gleicher Priorität werden Hooks in der Reihenfolge der Plugin-Registrierung ausgeführt.
- Hooks mit
dependencieswarten, 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:beforeSavelässt den Speichervorgang mitCONTENT_HOOK_ERRORscheitern. Geben Sie den dokumentiertenSAVE_REJECTED-Umschlag zurück, wenn der Redakteur einen konkreten Validierungsgrund sehen soll. - Wenn
content:beforeDeletefalsezurü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
| Hook | Auslöser | Rückgabe | Exklusiv |
|---|---|---|---|
plugin:install | Erstinstallation des Plugins | void | Nein |
plugin:activate | Plugin aktiviert | void | Nein |
plugin:deactivate | Plugin deaktiviert | void | Nein |
plugin:uninstall | Plugin entfernt | void | Nein |
content:beforeSave | Vor dem Speichern von Inhalten | Geänderte Inhalte, Ablehnungs-Umschlag oder void | Nein |
content:afterSave | Nach dem Speichern von Inhalten | void | Nein |
content:beforeDelete | Bevor Inhalte in den Papierkorb verschoben werden | false zum Abbrechen, sonst erlauben | Nein |
content:afterDelete | Nach Papierkorb oder dauerhaftem Löschen | void | Nein |
content:afterPublish | Nach dem Veröffentlichen von Inhalten | void | Nein |
content:afterUnpublish | Nach der Rücknahme der Veröffentlichung | void | Nein |
content:afterRestore | Nach dem Wiederherstellen von Inhalten | void | Nein |
content:afterSchedule | Nach dem Planen von Inhalten | void | Nein |
content:afterUnschedule | Nach dem Aufheben der Planung | void | Nein |
media:beforeUpload | Vor dem Datei-Upload | Geänderte Dateiinformationen oder void | Nein |
media:afterUpload | Nach dem Datei-Upload | void | Nein |
cron | Geplante Aufgabe wird ausgelöst | void | Nein |
email:beforeSend | Vor der E-Mail-Zustellung | Geänderte Nachricht, false oder void | Nein |
email:deliver | E-Mail über Transport zustellen | void | Ja |
email:afterSend | Nach der E-Mail-Zustellung | void | Nein |
comment:beforeCreate | Bevor ein Kommentar gespeichert wird | Geändertes Ereignis, false oder void | Nein |
comment:moderate | Kommentarstatus festlegen | { status, reason? } | Ja |
comment:afterCreate | Nach dem Speichern eines Kommentars | void | Nein |
comment:afterModerate | Admin ändert Kommentarstatus | void | Nein |
byline:afterSave | Nach dem Speichern einer Byline | void | Nein |
byline:afterDelete | Nach dem Löschen einer Byline | void | Nein |
page:metadata | Seiten-Rendering | Beiträge oder null | Nein |
page:fragments | Seiten-Rendering (nur nativ) | Beiträge oder null | Nein |
Vollständige Ereignistypen und Handler-Signaturen finden Sie in der Hook-Referenz.