Plugin-Hooks

Auf dieser Seite

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

OptionTypStandardBeschreibung
prioritynumber100Ausführungsreihenfolge. Kleinere Werte laufen zuerst.
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 stoppt.
exclusivebooleanfalseNur ein Plugin kann der aktive Provider sein. Verwendet für email:deliver und comment:moderate.
handlerfunctionDie 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:

  1. Hooks mit niedrigeren priority-Werten laufen zuerst
  2. Bei gleicher Priorität laufen Hooks in der Plugin-Registrierungsreihenfolge
  3. Hooks mit dependencies warten, 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:

ArtWird gerendert alsDedupe-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

HookAuslöserRückgabeExklusiv
plugin:installErste Plugin-InstallationvoidNein
plugin:activatePlugin aktiviertvoidNein
plugin:deactivatePlugin deaktiviertvoidNein
plugin:uninstallPlugin entferntvoidNein
content:beforeSaveVor dem Speichern von InhaltGeänderter 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
content:afterPublishNach Veröffentlichung von InhaltvoidNein
content:afterUnpublishNach Zurücknahme der VeröffentlichungvoidNein
media:beforeUploadVor dem Datei-UploadGeänderte Dateiinfos oder voidNein
media:afterUploadNach dem Datei-UploadvoidNein
cronGeplanter Task feuertvoidNein
email:beforeSendVor dem E-Mail-VersandGeänderte Nachricht, false oder voidNein
email:deliverE-Mail über Transport zustellenvoidJa
email:afterSendNach dem E-Mail-VersandvoidNein
comment:beforeCreateVor dem Speichern des KommentarsGeändertes Ereignis, false oder voidNein
comment:moderateKommentarstatus festlegen{ status, reason? }Ja
comment:afterCreateNach dem Speichern des KommentarsvoidNein
comment:afterModerateAdmin ändert KommentarstatusvoidNein
page:metadataSeiten-RenderBeiträge oder nullNein
page:fragmentsSeiten-Render (vertrauenswürdig)Beiträge oder nullNein

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