Mit Hooks können Plugins Code als Reaktion auf Ereignisse ausführen. Alle Hooks erhalten ein Ereignisobjekt und den Plugin-Kontext. Hooks werden bei der Plugin-Definition deklariert, nicht dynamisch zur Laufzeit registriert.
Hook-Signatur
Jeder Hook-Handler erhält zwei Argumente:
async (event: EventType, ctx: PluginContext) => ReturnType;
event— Daten zum Ereignis (gespeicherter Inhalt, hochgeladene Medien usw.)ctx— Der Plugin-Kontext mit Storage, KV, Logging und capability-geschützten APIs
Hook-Konfiguration
Hooks können als einfacher Handler oder mit vollständiger Konfiguration 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
| Option | Typ | Standard | Beschreibung |
|---|---|---|---|
priority | number | 100 | Ausführungsreihenfolge. Kleinere Werte laufen zuerst. |
timeout | number | 5000 | Maximale Ausführungszeit in Millisekunden. |
dependencies | string[] | [] | Plugin-IDs, die vor diesem Hook laufen müssen. |
errorPolicy | "abort" | "continue" | "abort" | Ob die Pipeline bei einem Fehler stoppt. |
exclusive | boolean | false | Nur ein Plugin kann der aktive Provider sein. Verwendet für email:deliver und comment:moderate. |
handler | function | — | Die Hook-Handler-Funktion. Erforderlich. |
Lifecycle-Hooks
Lifecycle-Hooks laufen bei Installation, Aktivierung und Deaktivierung des Plugins.
plugin:install
Wird einmal ausgeführt, wenn das Plugin einer Site erstmals 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" });
}
Ereignis: {}
Rückgabe: Promise<void>
plugin:activate
Wird ausgeführt, wenn das Plugin aktiviert wird (nach Installation oder 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 wird (aber nicht entfernt).
"plugin:deactivate": async (_event, ctx) => {
ctx.log.info("Plugin deactivated");
// Release resources, pause background work
}
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) {
// 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));
}
}
Ereignis: { deleteData: boolean }
Rückgabe: Promise<void>
Content-Hooks
Content-Hooks laufen bei Erstellen, Aktualisieren und Löschen.
content:beforeSave
Läuft, bevor Inhalt gespeichert wird. Geben Sie geänderten Inhalt oder void zurück, um ihn unverändert zu lassen. Werfen Sie 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;
}
Ereignis:
{
content: Record<string, unknown>; // Content data being saved
collection: string; // Collection name
isNew: boolean; // True if creating, false if updating
}
Rückgabe: Promise<Record<string, unknown> | void>
content:afterSave
Läuft, nachdem Inhalt erfolgreich gespeichert wurde. Nutzen Sie ihn für Nebenwirkungen wie Benachrichtigungen, Logging oder Sync mit externen Systemen.
"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 })
});
}
}
Ereignis:
{
content: Record<string, unknown>; // Saved content (includes id, timestamps)
collection: string;
isNew: boolean;
}
Rückgabe: Promise<void>
content:beforeDelete
Läuft, bevor Inhalt gelöscht wird. Geben Sie 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;
}
Ereignis:
{
id: string; // Content ID being deleted
collection: string;
}
Rückgabe: Promise<boolean | void>
content:afterDelete
Läuft, 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}`);
}
Ereignis:
{
id: string;
collection: string;
}
Rückgabe: Promise<void>
content:afterPublish
Läuft, nachdem Inhalt veröffentlicht wurde (von Entwurf auf live). Nutzen Sie ihn für Nebenwirkungen wie Cache-Invalidierung, Benachrichtigungen oder Sync mit externen Systemen.
Erfordert die Capability read:content.
"content:afterPublish": async (event, ctx) => {
const { content, collection } = event;
ctx.log.info(`Published ${collection}/${content.id}`);
// Notify external system
if (ctx.http) {
await ctx.http.fetch("https://api.example.com/webhook", {
method: "POST",
body: JSON.stringify({ event: "content:publish", id: content.id })
});
}
}
Ereignis:
{
content: Record<string, unknown>; // Published content (includes id, timestamps)
collection: string;
}
Rückgabe: Promise<void>
content:afterUnpublish
Läuft, nachdem die Veröffentlichung eines Inhalts zurückgenommen wurde (von live zurück auf Entwurf). Nutzen Sie ihn für Nebenwirkungen wie Cache-Invalidierung oder Benachrichtigung externer Systeme.
Erfordert die Capability read:content.
"content:afterUnpublish": async (event, ctx) => {
const { content, collection } = event;
ctx.log.info(`Unpublished ${collection}/${content.id}`);
}
Ereignis:
{
content: Record<string, unknown>; // Unpublished content
collection: string;
}
Rückgabe: Promise<void>
Media-Hooks
Media-Hooks laufen beim Hochladen von Dateien.
media:beforeUpload
Läuft, bevor eine Datei hochgeladen wird. Geben Sie geänderte Dateiinfos oder void zurück. Werfen Sie 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}`
};
}
Ereignis:
{
file: {
name: string; // Original filename
type: string; // MIME type
size: number; // Size in bytes
}
}
Rückgabe: Promise<{ name: string; type: string; size: number } | void>
media:afterUpload
Läuft, 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
});
}
Ereignis:
{
media: {
id: string;
filename: string;
mimeType: string;
size: number | null;
url: string;
createdAt: string;
}
}
Rückgabe: Promise<void>
Ausführungsreihenfolge der Hooks
Hooks laufen in dieser Reihenfolge:
- Hooks mit niedrigeren
priority-Werten laufen zuerst - Bei gleicher Priorität laufen Hooks in der Plugin-Registrierungsreihenfolge
- Hooks mit
dependencieswarten, bis diese Plugins fertig sind
// 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 () => {}
}
Fehlerbehandlung
Wenn ein Hook eine Exception wirft oder das Timeout erreicht:
errorPolicy: "abort"— Die gesamte Pipeline stoppt. Die ursprüngliche Operation kann fehlschlagen.errorPolicy: "continue"— Der Fehler wird protokolliert, verbleibende Hooks laufen weiter.
"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");
}
}
Timeouts
Hooks haben standardmäßig ein Timeout von 5000 ms (5 Sekunden). Erhöhen Sie es für länger dauernde Operationen:
"content:afterSave": {
timeout: 30000, // 30 seconds
handler: async (event, ctx) => {
// Long-running operation
}
}
Öffentliche Seiten-Hooks
Öffentliche Seiten-Hooks ermöglichen Beiträge zum <head> und <body> gerenderter Seiten. Templates melden sich mit den Komponenten <EmDashHead>, <EmDashBodyStart> und <EmDashBodyEnd> aus emdash/ui an.
page:metadata
Liefert typisierte Metadaten für <head> — Meta-Tags, OpenGraph-Eigenschaften, Canonical-/Alternate-Links und JSON-LD-Strukturdaten. Funktioniert im vertrauenswürdigen und im Sandbox-Modus.
Der Kern validiert, dedupliziert und rendert die Beiträge. Plugins liefern strukturierte Daten, 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 };
}
}
Rückgabe: PageMetadataContribution | PageMetadataContribution[] | null
Beitragstypen:
| Art | Wird gerendert als | 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 pro Dedupe-Schlüssel. Link-href müssen HTTP oder HTTPS sein.
page:fragments
Liefert rohes HTML, Skripte oder Markup an Einfügepunkten der Seite. Nur vertrauenswürdige Plugins — sandgeboxte Plugins können diesen Hook nicht nutzen.
"page:fragments": async (event, ctx) => {
return {
kind: "external-script",
placement: "head",
src: "https://www.googletagmanager.com/gtm.js?id=GTM-XXXXX",
async: true,
};
}
Rückgabe: PageFragmentContribution | PageFragmentContribution[] | null
Platzierungen: "head", "body:start", "body:end". Templates ohne Komponente für eine Platzierung ignorieren still Beiträge dafür.
Hooks-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 dem Speichern von Inhalt | Geänderter Inhalt oder void | Nein |
content:afterSave | Nach dem Speichern von Inhalt | void | Nein |
content:beforeDelete | Vor dem Löschen von Inhalt | false zum Abbrechen, sonst erlauben | Nein |
content:afterDelete | Nach dem Löschen von Inhalt | void | Nein |
content:afterPublish | Nach Veröffentlichung von Inhalt | void | Nein |
content:afterUnpublish | Nach Zurücknahme der Veröffentlichung | void | Nein |
media:beforeUpload | Vor dem Datei-Upload | Geänderte Dateiinfos oder void | Nein |
media:afterUpload | Nach dem Datei-Upload | void | Nein |
cron | Geplanter Task feuert | void | Nein |
email:beforeSend | Vor dem E-Mail-Versand | Geänderte Nachricht, false oder void | Nein |
email:deliver | E-Mail über Transport zustellen | void | Ja |
email:afterSend | Nach dem E-Mail-Versand | void | Nein |
comment:beforeCreate | Vor dem Speichern des Kommentars | Geändertes Ereignis, false oder void | Nein |
comment:moderate | Kommentarstatus festlegen | { status, reason? } | Ja |
comment:afterCreate | Nach dem Speichern des Kommentars | void | Nein |
comment:afterModerate | Admin ändert Kommentarstatus | void | Nein |
page:metadata | Seiten-Render | Beiträge oder null | Nein |
page:fragments | Seiten-Render (vertrauenswürdig) | Beiträge oder null | Nein |
Siehe die Hook-Referenz für vollständige Ereignistypen und Handler-Signaturen.