Hook-Referenz

Auf dieser Seite

Mit Hooks können Plugins das Verhalten von EmDash an bestimmten Punkten im Lebenszyklus von Inhalten, Medien, E-Mails, Kommentaren und Seiten abfangen und ändern.

Hook-Überblick

Die folgende Tabelle listet jeden Hook, seinen Auslöser, was er ändern kann und ob er exklusiv ist:

HookAuslöserKann ändernExklusiv
content:beforeSaveBevor Inhalt gespeichert wirdInhaltsdatenNein
content:afterSaveNach dem Speichern von InhaltNichtsNein
content:beforeDeleteBevor Inhalt gelöscht wirdKann abbrechenNein
content:afterDeleteNach dem Löschen von InhaltNichtsNein
content:beforePublishBevor Inhalt veröffentlicht wirdKann abbrechenNein
content:beforeScheduleBevor Inhalt geplant wirdKann abbrechenNein
content:beforeUnpublishBevor Veröffentlichung aufgehoben wirdKann abbrechenNein
content:afterPublishNach der Veröffentlichung von InhaltNichtsNein
content:afterUnpublishNach Aufheben der VeröffentlichungNichtsNein
content:afterRestoreNach Wiederherstellung von InhaltNichtsNein
content:afterScheduleNach Planung von InhaltNichtsNein
content:afterUnscheduleNach Aufheben der PlanungNichtsNein
media:beforeUploadBevor eine Datei hochgeladen wirdDateimetadatenNein
media:afterUploadNach dem Hochladen einer DateiNichtsNein
cronGeplante Aufgabe wird ausgeführtNichtsNein
email:beforeSendBevor E-Mail zugestellt wirdNachricht, kann abbrechenNein
email:deliverE-Mail über Transport zustellenNichtsJa
email:afterSendNach erfolgreicher E-Mail-ZustellungNichtsNein
comment:beforeCreateBevor Kommentar gespeichert wirdKommentar, kann abbrechenNein
comment:moderateFreigabestatus des Kommentars festlegenStatusJa
comment:afterCreateNach dem Speichern eines KommentarsNichtsNein
comment:afterModerateNach Änderung des Kommentarstatus durch AdminNichtsNein
page:metadataRendern des öffentlichen Seiten-HeadsTags beisteuernNein
page:fragmentsRendern des öffentlichen Seiten-BodySkripte einfügenNein
plugin:installBei Erstinstallation des PluginsNichtsNein
plugin:activateWenn Plugin aktiviert wirdNichtsNein
plugin:deactivateWenn Plugin deaktiviert wirdNichtsNein
plugin:uninstallWenn Plugin entfernt wirdNichtsNein

Content-Hooks

content:beforeSave

Capability: content:write

Wird ausgeführt, bevor Inhalt in der Datenbank gespeichert wird. Nutzen Sie ihn zum Validieren, Transformieren oder Anreichern von Inhalt. Ein Hook in der Sandbox lehnt das Speichern ab, indem er ein Hook-Ergebnis der Version 1 mit einem SAVE_REJECTED-Fehler und einem Klartext-reason von 1–500 Zeichen zurückgibt. Die API antwortet mit SAVE_REJECTED, und das Admin-Panel identifiziert das Plugin und zeigt den Grund als Text. Leere, zu lange, fehlerhafte und unbekannte Fehlerergebnisse führen zum Speicherfehler mit einer generischen CONTENT_HOOK_ERROR-Antwort.

Im Host-Prozess werfen Sie ContentSaveRejectedError (exportiert aus emdash), um ein Speichern abzulehnen. Jede andere Ausnahme aus beiden Ausführungsmodi führt zum Speicherfehler mit einer generischen Antwort, die die Ausnahmemeldung nicht preisgibt.

import { definePlugin } from "emdash";

export default definePlugin({
	id: "my-plugin",
	version: "1.0.0",
	hooks: {
		"content:beforeSave": async (event, ctx) => {
			const { content, collection, isNew } = event;

			// Add timestamps
			if (isNew) {
				content.createdBy = "system";
			}
			content.modifiedAt = new Date().toISOString();

			// Return modified content
			return content;
		},
	},
});

Ereignis

interface ActorInfo {
	readonly id: string;
	readonly role: number;
}

interface ContentHookEvent {
	content: Record<string, unknown>; // Content data
	collection: string; // Collection slug
	isNew: boolean; // True for creates, false for updates
	id?: string; // ID of the existing item on updates; absent on creates
	actor?: ActorInfo; // Authenticated user that initiated the save
}

Bei einem Update enthält content nur die übermittelten Feldwerte. Laden Sie den gespeicherten Eintrag mit ctx.content.get(event.collection, event.id), wenn der Hook einen Vergleich damit benötigt. Authentifizierte REST-, Visual-Editing- und MCP-Speichervorgänge enthalten actor. Interne Schreibvorgänge ohne authentifizierten Benutzer lassen ihn weg.

Rückgabewert

  • Geben Sie das geänderte Inhaltsobjekt zurück, um Änderungen anzuwenden
  • Geben Sie eine Sandbox-Hook-Fehlerhülle zurück, um das Speichern mit einem begrenzten Klartextgrund abzulehnen
  • Geben Sie void zurück, um unverändert durchzureichen

Ein Hook in der Sandbox gibt diese vollständige Hülle zurück, um ein Speichern abzulehnen:

return {
	__emdashSandboxHookResult: true,
	version: 1,
	error: {
		code: "SAVE_REJECTED",
		reason: "Add a summary before saving.",
	},
};

Der Host trimmt reason und akzeptiert zwischen 1 und 500 Zeichen.

content:afterSave

Capability: content:read

Wird nach dem Speichern von Inhalt ausgeführt. Nutzen Sie ihn für Nebenwirkungen wie Benachrichtigungen, Cache-Invalidierung oder externe Synchronisation.

hooks: {
  "content:afterSave": async (event, ctx) => {
    const { content, collection, isNew } = event;

    if (collection === "posts" && content.status === "published") {
      // Notify external service
      await ctx.http?.fetch("https://api.example.com/notify", {
        method: "POST",
        body: JSON.stringify({ postId: content.id }),
      });
    }
  },
}

Ereignis

content:afterSave erhält content, collection, isNew und den optionalen authentifizierten actor. content ist der vollständige gespeicherte Eintrag mit seiner Datenbank-ID in content.id und Collection-Feldern unter content.data. Die separate optionale id für Updates bei content:beforeSave fehlt nach dem Speichern.

Rückgabewert

Es wird kein Rückgabewert erwartet.

content:beforeDelete

Capability: content:read

Wird ausgeführt, bevor Inhalt gelöscht wird. Nutzen Sie ihn zur Validierung der Löschung oder um sie zu verhindern.

hooks: {
  "content:beforeDelete": async (event, ctx) => {
    const { id, collection } = event;

    // Prevent deletion of protected content
    const item = await ctx.content?.get(collection, id);
    if (item?.data.protected) {
      return false; // Cancel deletion
    }

    // Allow deletion
    return true;
  },
}

Ereignis

interface ContentDeleteEvent {
	id: string; // Entry ID
	collection: string; // Collection slug
	permanent?: false; // Present for native plugins; omitted in the sandbox runtime
}

content:beforeDelete läuft nur, wenn ein Eintrag in den Papierkorb verschoben wird. Native Plugins erhalten permanent: false; die Sandbox-Laufzeit sendet nur id und collection. Verzweigen Sie in einem Hook in der Sandbox nicht anhand dieses Felds. Permanente Löschung umgeht content:beforeDelete, sodass der Hook einen Administrator nicht daran hindern kann, einen Eintrag im Papierkorb endgültig zu löschen.

Rückgabewert

  • Geben Sie false zurück, um die Löschung abzubrechen
  • Geben Sie true oder void zurück, um zu erlauben

content:afterDelete

Capability: content:read

Wird nach dem Löschen von Inhalt ausgeführt. Nutzen Sie ihn für Aufräumarbeiten.

hooks: {
  "content:afterDelete": async (event, ctx) => {
    const { id, collection, permanent } = event;

    if (permanent) {
      await ctx.storage.relatedItems.delete(`${collection}:${id}`);
    }
  },
}

Das Ereignis enthält id, collection und permanent. permanent ist false, wenn der Eintrag in den Papierkorb verschoben wird, und true bei endgültiger Löschung. Prüfen Sie es, bevor Sie Daten entfernen, die ein wiederhergestellter Papierkorb-Eintrag noch benötigen würde. Dieser Hook hat keinen Rückgabewert.

content:beforePublish

Capability: hooks.content-policy:register

Wird ausgeführt, bevor Inhalt veröffentlicht wird. Geben Sie void zurück, um die Veröffentlichung zu erlauben, oder { cancel: true, reason }, um sie abzulehnen. EmDash akzeptiert 1–500 Klartextzeichen in reason und gibt bei explizitem Abbruch PUBLISH_REJECTED zurück.

Das Ereignis enthält den aktuellen content, collection und origin. Authentifizierte API-, MCP- und Visual-Editor-Aktionen enthalten außerdem actor mit der passenden source. Der Ursprung visual-editor erfordert das signierte, kurzlebige Action-Token aus einem authentifizierten Toolbar-Render. Geplante Einträge führen diesen Hook erneut aus, wenn sie fällig werden. Eine Ablehnung durch den Scheduler hebt die Planung auf und listet betroffenen Eintrag und Grund im Dashboard. Erfolgreiche Planung, Veröffentlichung oder Löschung löscht den Datensatz; ein Administrator kann einen veralteten Datensatz verwerfen.

content:beforeSchedule

Capability: hooks.content-policy:register

Wird ausgeführt, bevor ein Eintrag eine Veröffentlichungszeit erhält. Er verwendet dasselbe Entscheidungsformat und dieselben Origin-Felder wie content:beforePublish, fügt scheduledAt zum Ereignis hinzu und gibt bei explizitem Abbruch SCHEDULE_REJECTED zurück.

Es gibt keinen Hook content:beforeUnschedule. Ein Administrator kann eine zukünftige Veröffentlichung jederzeit abbrechen.

content:beforeUnpublish

Capability: hooks.content-policy:register

Wird ausgeführt, bevor live veröffentlichter Inhalt entfernt wird. Er verwendet dasselbe Entscheidungsformat und dieselben Origin-Felder wie content:beforePublish und gibt bei explizitem Abbruch UNPUBLISH_REJECTED zurück.

hooks.content-policy:register impliziert nicht content:read, content:write oder Veröffentlichungsaktionen. Ungültige Entscheidungen und unerwartete Abort-Policy-Fehler schlagen fehlgeschlossen ab, ohne Ausnahmemeldungen preiszugeben.

content:afterPublish

Wird ausgeführt, nachdem ein Eintrag erfolgreich veröffentlicht wurde, einschließlich eines Eintrags, den EmDash automatisch zur geplanten Zeit veröffentlicht. Nutzen Sie ihn für Arbeit, die vom veröffentlichten Eintrag abhängt, z. B. Benachrichtigung eines anderen Dienstes oder Aktualisierung eines externen Suchindex.

hooks: {
  "content:afterPublish": async (event, ctx) => {
    ctx.log.info(`Published ${event.collection}/${event.content.id}`);
  },
}

Der Hook erfordert die Capability content:read. EmDash führt ihn nach der Veröffentlichungsantwort aus, sodass sein Rückgabewert den Eintrag nicht ändern oder die Veröffentlichung rückgängig machen kann. Ein Fehler wird protokolliert; bei errorPolicy: "abort" laufen spätere Publish-Hooks nicht.

content:afterUnpublish

Wird ausgeführt, nachdem ein Eintrag erfolgreich depubliziert wurde. Nutzen Sie ihn, um Kopien von Inhalt in externen Systemen zu entfernen oder zu aktualisieren. Depublizieren bricht auch ausstehende Planungen ab, ohne content:afterUnschedule auszuführen.

hooks: {
  "content:afterUnpublish": async (event, ctx) => {
    ctx.log.info(`Unpublished ${event.collection}/${event.content.id}`);
  },
}

Dieser Hook hat dieselbe Capability-Anforderung content:read, verzögerte Ausführung, Ereignisform und den Vertrag ohne Rückgabewert wie content:afterPublish.

content:afterRestore

Wird ausgeführt, nachdem Inhalt aus dem Papierkorb wiederhergestellt wurde. Erfordert die Capability content:read. Der wiederhergestellte Eintrag ist ein Entwurf ohne Planung, unabhängig vom Status beim Verschieben in den Papierkorb. Das Entfernen der Planung führt nicht zusätzlich content:afterUnschedule aus.

hooks: {
  "content:afterRestore": async (event, ctx) => {
    ctx.log.info(`Restored ${event.collection}/${event.content.id}`);
  },
}

content:afterSchedule

Wird ausgeführt, nachdem Inhalt für eine zukünftige Veröffentlichung geplant wurde. Erfordert die Capability content:read.

hooks: {
  "content:afterSchedule": async (event, ctx) => {
    ctx.log.info(`Scheduled ${event.collection}/${event.content.id}`);
  },
}

content:afterUnschedule

Wird ausgeführt, nachdem die Planung von Inhalt aufgehoben wurde. Erfordert die Capability content:read.

hooks: {
  "content:afterUnschedule": async (event, ctx) => {
    ctx.log.info(`Unscheduled ${event.collection}/${event.content.id}`);
  },
}

Ereignis

interface ContentStateChangeEvent {
	content: Record<string, unknown>;
	collection: string;
}

Diese Ereignisform teilen content:afterPublish, content:afterUnpublish, content:afterRestore, content:afterSchedule und content:afterUnschedule. content ist der vollständige Eintrag nach der Statusänderung, einschließlich id, slug und Status; Collection-Felder liegen unter content.data.

Rückgabewert

Es wird kein Rückgabewert erwartet.

Media-Hooks

media:beforeUpload

Capability: media:write

Wird ausgeführt, bevor eine Datei hochgeladen wird. Nutzen Sie ihn zum Validieren, Umbenennen oder Ablehnen von Dateien.

hooks: {
  "media:beforeUpload": async (event, ctx) => {
    const { file } = event;

    // Reject files over 10MB
    if (file.size > 10 * 1024 * 1024) {
      throw new Error("File too large");
    }

    // Rename file
    return {
      name: `${Date.now()}-${file.name}`,
      type: file.type,
      size: file.size,
    };
  },
}

Ereignis

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

Rückgabewert

  • Geben Sie geänderte Dateimetadaten zurück, um Änderungen anzuwenden
  • Geben Sie void zurück, um unverändert durchzureichen
  • Werfen Sie eine Ausnahme, um den Upload abzulehnen

media:afterUpload

Capability: media:read

Wird nach dem Hochladen einer Datei ausgeführt. Nutzen Sie ihn für Verarbeitung, Vorschaubilder oder Metadaten-Extraktion.

hooks: {
  "media:afterUpload": async (event, ctx) => {
    const { media } = event;

    if (media.mimeType.startsWith("image/")) {
      // Store image metadata
      await ctx.kv.set(`media:${media.id}:analyzed`, {
        processedAt: new Date().toISOString(),
      });
    }
  },
}

Ereignis

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

Rückgabewert

Es wird kein Rückgabewert erwartet.

Lifecycle-Hooks

Lifecycle-Hooks erfordern keine Registrierungs-Capability.

plugin:install

Wird ausgeführt, wenn ein Plugin erstmals installiert wird. Nutzen Sie ihn für Ersteinrichtung, Anlegen von Storage-Collections oder Seed-Daten.

hooks: {
  "plugin:install": async (event, ctx) => {
    // Initialize default settings
    await ctx.settings.set("enabled", true);
    await ctx.settings.set("threshold", 100);

    ctx.log.info("Plugin installed successfully");
  },
}

plugin:activate

Wird ausgeführt, wenn ein Plugin aktiviert wird (nach Installation oder erneuter Aktivierung).

hooks: {
  "plugin:activate": async (event, ctx) => {
    ctx.log.info("Plugin activated");
  },
}

plugin:deactivate

Wird ausgeführt, wenn ein Plugin deaktiviert wird.

hooks: {
  "plugin:deactivate": async (event, ctx) => {
    ctx.log.info("Plugin deactivated");
  },
}

plugin:install, plugin:activate und plugin:deactivate erhalten ein leeres Ereignisobjekt. Sie haben keinen Rückgabewert.

plugin:uninstall

Wird ausgeführt, wenn ein Plugin entfernt wird. Nutzen Sie ihn für Aufräumen.

hooks: {
  "plugin:uninstall": async (event, ctx) => {
    const { deleteData } = event;

    if (deleteData) {
      // Clean up all plugin data
      const items = await ctx.settings.list();
      for (const { key } of items) {
        await ctx.kv.delete(key);
      }
    }

    ctx.log.info("Plugin uninstalled");
  },
}

Ereignis

interface UninstallEvent {
	deleteData: boolean; // User chose to delete data
}

Der Deinstallations-Hook hat keinen Rückgabewert.

Cron-Hook

cron

Capability: Keine erforderlich

Wird ausgelöst, wenn eine geplante Aufgabe ausgeführt wird. Planen Sie Aufgaben mit ctx.cron.schedule(). Cron-Ausdrücke werden auf jedem Host in UTC ausgewertet.

hooks: {
  "cron": async (event, ctx) => {
    if (event.name === "daily-sync") {
      const data = await ctx.http?.fetch("https://api.example.com/data");
      ctx.log.info("Sync complete");
    }
  },
}

Ereignis

interface CronEvent {
	name: string;
	data?: Record<string, unknown>;
	scheduledAt: string;
}

Der Cron-Hook hat keinen Rückgabewert.

E-Mail-Hooks

Bei von Plugins gesendeten Nachrichten laufen E-Mail-Hooks in dieser Reihenfolge: email:beforeSend, dann email:deliver, dann email:afterSend. System-Authentifizierungsnachrichten gehen direkt an email:deliver; sie durchlaufen nicht email:beforeSend oder email:afterSend.

email:beforeSend

Capability: hooks.email-events:register

Middleware-Hook vor der Zustellung. Nachrichten transformieren oder Zustellung abbrechen.

hooks: {
  "email:beforeSend": async (event, ctx) => {
    // Add footer to all emails
    return {
      ...event.message,
      text: event.message.text + "\n\n—Sent from My Site",
    };

    // Or return false to cancel delivery
  },
}

Ereignis

interface EmailBeforeSendEvent {
	message: {
		to: string;
		cc?: string[];
		replyTo?: string;
		subject: string;
		text: string;
		html?: string;
	};
	source: string;
}

Rückgabewert

  • Geben Sie die geänderte Nachricht zurück, um zu transformieren
  • Geben Sie false zurück, um die Zustellung abzubrechen

email:deliver

Capability: hooks.email-transport:register | Exklusiv: Ja

Der Transport-Provider. Nur ein Plugin kann E-Mails zustellen. Verantwortlich für das tatsächliche Senden der Nachricht über einen E-Mail-Dienst.

hooks: {
  "email:deliver": {
    exclusive: true,
    handler: async (event, ctx) => {
      await sendViaSES(event.message);
    },
  },
}

Ereignis und Rückgabewert

interface EmailDeliverEvent {
	message: {
		to: string;
		cc?: string[];
		replyTo?: string;
		subject: string;
		text: string;
		html?: string;
	};
	source: string;
}

Der Hook hat keinen Rückgabewert. source ist "system" für EmDash-Authentifizierungsnachrichten und die Plugin-ID für von einem Plugin gesendete Nachrichten.

Plugins können cc (zusätzliche Empfänger) und replyTo in einer Nachricht setzen. Provider sollten beides zustellen, wenn vorhanden; ein Provider, der cc stillschweigend verwirft, lässt diese Empfänger ohne Nachricht zurück.

email:afterSend

Capability: hooks.email-events:register

Fire-and-Forget-Hook nach erfolgreicher Zustellung. Fehler werden protokolliert, propagieren aber nicht.

hooks: {
  "email:afterSend": async (event, ctx) => {
    await ctx.kv.set(`email:log:${Date.now()}`, {
      to: event.message.to,
      subject: event.message.subject,
    });
  },
}

Ereignis und Rückgabewert

email:afterSend erhält dieselben Felder message und source wie email:deliver. Er hat keinen Rückgabewert.

Kommentar-Hooks

Kommentar-Hooks laufen in dieser Reihenfolge: comment:beforeCreate, dann comment:moderate, dann comment:afterCreate. Der Hook comment:afterModerate läuft separat, wenn ein Administrator oder ein autorisiertes Plugin den Status eines Kommentars ändert.

Nach dem Speichern eines Kommentars erhalten Hooks diese Datensatzform:

interface StoredComment {
	id: string;
	collection: string;
	contentId: string;
	parentId: string | null;
	authorName: string;
	authorEmail: string;
	authorUserId: string | null;
	body: string;
	status: string;
	moderationMetadata: Record<string, unknown> | null;
	createdAt: string;
	updatedAt: string;
}

comment:beforeCreate

Capability: users:read

Middleware-Hook, bevor ein Kommentar gespeichert wird. Kommentare anreichern, validieren oder ablehnen.

hooks: {
  "comment:beforeCreate": async (event, ctx) => {
    // Reject comments with links
    if (event.comment.body.includes("http")) {
      return false;
    }
  },
}

Ereignis

interface CommentBeforeCreateEvent {
	comment: {
		collection: string;
		contentId: string;
		parentId: string | null;
		authorName: string;
		authorEmail: string;
		authorUserId: string | null;
		body: string;
		ipHash: string | null;
		userAgent: string | null;
	};
	metadata: Record<string, unknown>;
}

Rückgabewert

  • Geben Sie das geänderte Ereignis zurück, um zu transformieren
  • Geben Sie false zurück, um abzulehnen
  • Geben Sie void zurück, um durchzureichen

comment:moderate

Capability: users:read | Exklusiv: Ja

Entscheidet, ob ein Kommentar freigegeben, ausstehend oder Spam ist. Nur ein Moderations-Provider ist aktiv.

hooks: {
  "comment:moderate": {
    exclusive: true,
    handler: async (event, ctx) => {
      const score = await checkSpam(event.comment);
      return {
        status: score > 0.8 ? "spam" : score > 0.5 ? "pending" : "approved",
        reason: `Spam score: ${score}`,
      };
    },
  },
}

Ereignis

interface CommentModerateEvent {
	comment: { /* same as beforeCreate */ };
	metadata: Record<string, unknown>;
	collectionSettings: {
		commentsEnabled: boolean;
		commentsModeration: "all" | "first_time" | "none";
		commentsClosedAfterDays: number;
		commentsAutoApproveUsers: boolean;
	};
	priorApprovedCount: number;
}

Rückgabewert

{ status: "approved" | "pending" | "spam"; reason?: string }

Aktiver Moderator

EmDash enthält einen eingebauten Moderator, der die Kommentareinstellungen der Collection anwendet. Wenn genau ein Plugin comment:moderate bereitstellt, wählt EmDash dieses Plugin statt des eingebauten Moderators und speichert die Wahl. Wenn mehrere Plugins es bereitstellen und keine Wahl gespeichert ist, wählt EmDash keines, und neue Kommentare warten auf Prüfung. Eine gespeicherte Wahl, einschließlich des eingebauten Moderators, bleibt aktiv, solange der gewählte Moderator aktiviert ist.

comment:afterCreate

Capability: users:read

Fire-and-Forget-Hook nach dem Speichern eines Kommentars. Nutzen Sie ihn für Benachrichtigungen. E-Mail-Versand erfordert außerdem die Capability email:send und einen konfigurierten email:deliver-Provider; ohne beides ist ctx.email undefined.

hooks: {
  "comment:afterCreate": async (event, ctx) => {
    const recipient = event.contentAuthor?.email;
    if (event.comment.status === "approved" && recipient && ctx.email) {
      await ctx.email.send({
        to: recipient,
        subject: `New comment on "${event.content.title}"`,
        text: `${event.comment.authorName} commented: ${event.comment.body}`,
      });
    }
  },
}

Ereignis und Rückgabewert

interface CommentAfterCreateEvent {
	comment: StoredComment;
	metadata: Record<string, unknown>;
	content: { id: string; collection: string; slug: string; title?: string };
	contentAuthor?: { id: string; name: string | null; email: string };
}

Der Hook hat keinen Rückgabewert.

comment:afterModerate

Capability: users:read

Wird ausgeführt, nachdem ein Administrator oder ein Plugin den Status eines Kommentars geändert hat. Eine erfolgreiche Transition führt den Hook einmal aus. Hook-Fehler werden protokolliert und machen die Statusänderung nicht rückgängig.

Ereignis

interface CommentAfterModerateEvent {
	comment: StoredComment;
	previousStatus: string;
	newStatus: string;
	moderator: { id: string; name: string | null };
	origin?:
		| { source: "admin"; userId: string }
		| { source: "plugin"; pluginId: string };
}

Der Hook hat keinen Rückgabewert.

Seiten-Hooks

Seiten-Hooks laufen beim Rendern öffentlicher Seiten. Sie ermöglichen Plugins, Metadaten und Skripte einzubinden.

Beide Seiten-Hooks erhalten den aktuellen öffentlichen Seitenkontext:

interface PublicPageContext {
	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;
}

interface PageMetadataEvent { page: PublicPageContext }
interface PageFragmentEvent { page: PublicPageContext }

page:metadata

Capability: Keine erforderlich

Meta-Tags, Open-Graph-Eigenschaften, JSON-LD-Strukturdaten oder Link-Tags zum Seiten-Head beisteuern.

hooks: {
  "page:metadata": async (event, ctx) => {
    return [
      { kind: "meta", name: "generator", content: "EmDash" },
      { kind: "property", property: "og:site_name", content: event.page.siteName ?? "My Site" },
      { kind: "jsonld", graph: { "@type": "WebSite", name: event.page.siteName } },
    ];
  },
}

Beitragstypen

type PageMetadataContribution =
	| { kind: "meta"; name: string; content: string; key?: string }
	| { kind: "property"; property: string; content: string; key?: string }
	| {
			kind: "link";
			rel: "canonical" | "alternate" | "author" | "license" | "nlweb" | "site.standard.document";
			href: string;
			hreflang?: string;
			key?: string;
	  }
	| {
			kind: "jsonld";
			id?: string;
			graph: Record<string, unknown> | Array<Record<string, unknown>>;
	  };

Das Feld key dedupliziert Beiträge — nur der letzte Beitrag mit einem gegebenen Schlüssel wird verwendet.

Geben Sie einen Beitrag, ein Array von Beiträgen oder null zurück, wenn das Plugin nichts hinzufügen soll.

page:fragments

Capability: hooks.page-fragments:register

Skripte oder HTML in Seiten einfügen. Nur für native Plugins verfügbar.

hooks: {
  "page:fragments": async (event, ctx) => {
    return [
      {
        kind: "external-script",
        placement: "body:end",
        src: "https://analytics.example.com/script.js",
        async: true,
      },
      {
        kind: "inline-script",
        placement: "head",
        code: `window.siteId = "abc123";`,
      },
    ];
  },
}

Beitragstypen

type PageFragmentContribution =
	| {
			kind: "external-script";
			placement: "head" | "body:start" | "body:end";
			src: string;
			async?: boolean;
			defer?: boolean;
			attributes?: Record<string, string>;
			key?: string;
		}
	| {
			kind: "inline-script";
			placement: "head" | "body:start" | "body:end";
			code: string;
			attributes?: Record<string, string>;
			key?: string;
		}
	| {
			kind: "html";
			placement: "head" | "body:start" | "body:end";
			html: string;
			key?: string;
		};

Geben Sie einen Fragment-Beitrag, ein Array von Beiträgen oder null zurück, wenn das Plugin nichts hinzufügen soll.

Hook-Konfiguration

Hooks akzeptieren entweder eine Handler-Funktion oder ein Konfigurationsobjekt:

hooks: {
  // Simple handler
  "content:afterSave": async (event, ctx) => { ... },

  // With configuration
  "content:beforeSave": {
    priority: 50,        // Lower runs first (default: 100)
    timeout: 10000,      // Max execution time in ms (default: 5000)
    dependencies: [],    // Run after these plugins
    errorPolicy: "abort", // "continue" or "abort" (default)
    handler: async (event, ctx) => { ... },
  },
}

Konfigurationsoptionen

OptionTypStandardBeschreibung
prioritynumber100Ausführungsreihenfolge (niedriger = früher)
timeoutnumber5000Maximale Ausführungszeit in Millisekunden
dependenciesstring[][]Plugin-IDs, die zuerst laufen müssen
errorPolicystring"abort""continue", um Fehler zu ignorieren
exclusivebooleanfalseNur ein Plugin kann aktiver Provider sein (bei Provider-Pattern-Hooks wie email:deliver, comment:moderate)

Plugin-Kontext

Alle Hooks erhalten ein Kontextobjekt mit Zugriff auf Plugin-APIs:

interface PluginContext {
	plugin: { id: string; version: string };
	storage: PluginStorage;
	kv: KVAccess;
	content?: ContentAccess;
	media?: MediaAccess;
	http?: HttpAccess;
	log: LogAccess;
	site: { name: string; url: string; locale: string };
	url(path: string): string;
	users?: UserAccess;
	cron?: CronAccess;
	email?: EmailAccess;
}

Siehe Plugin-Fähigkeiten für die von jeder Kontext-API erforderliche Capability.

Fehlerbehandlung

Fehler in Hooks werden protokolliert und gemäß errorPolicy behandelt:

  • "abort" (Standard) — Ausführung stoppen, Transaktion ggf. zurückrollen
  • "continue" — Fehler protokollieren und mit dem nächsten Hook fortfahren
hooks: {
  "content:beforeSave": {
    errorPolicy: "continue", // Don't block save if this fails
    handler: async (event, ctx) => {
      try {
        await ctx.http?.fetch("https://api.example.com/validate");
      } catch (error) {
        ctx.log.warn("Validation service unavailable", error);
      }
    },
  },
}

Ausführungsreihenfolge

Hooks laufen in dieser Reihenfolge:

  1. Sortiert nach priority (aufsteigend)
  2. Plugins mit dependencies laufen nach ihren Abhängigkeiten
  3. Bei gleicher Priorität ist die Reihenfolge deterministisch, aber nicht spezifiziert
// This runs first (priority 10)
{ priority: 10, handler: ... }

// This runs second (priority 50)
{ priority: 50, handler: ... }

// This runs last (default priority 100)
{ handler: ... }