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:
| Hook | Auslöser | Kann ändern | Exklusiv |
|---|---|---|---|
content:beforeSave | Bevor Inhalt gespeichert wird | Inhaltsdaten | Nein |
content:afterSave | Nach dem Speichern von Inhalt | Nichts | Nein |
content:beforeDelete | Bevor Inhalt gelöscht wird | Kann abbrechen | Nein |
content:afterDelete | Nach dem Löschen von Inhalt | Nichts | Nein |
content:beforePublish | Bevor Inhalt veröffentlicht wird | Kann abbrechen | Nein |
content:beforeSchedule | Bevor Inhalt geplant wird | Kann abbrechen | Nein |
content:beforeUnpublish | Bevor Veröffentlichung aufgehoben wird | Kann abbrechen | Nein |
content:afterPublish | Nach der Veröffentlichung von Inhalt | Nichts | Nein |
content:afterUnpublish | Nach Aufheben der Veröffentlichung | Nichts | Nein |
content:afterRestore | Nach Wiederherstellung von Inhalt | Nichts | Nein |
content:afterSchedule | Nach Planung von Inhalt | Nichts | Nein |
content:afterUnschedule | Nach Aufheben der Planung | Nichts | Nein |
media:beforeUpload | Bevor eine Datei hochgeladen wird | Dateimetadaten | Nein |
media:afterUpload | Nach dem Hochladen einer Datei | Nichts | Nein |
cron | Geplante Aufgabe wird ausgeführt | Nichts | Nein |
email:beforeSend | Bevor E-Mail zugestellt wird | Nachricht, kann abbrechen | Nein |
email:deliver | E-Mail über Transport zustellen | Nichts | Ja |
email:afterSend | Nach erfolgreicher E-Mail-Zustellung | Nichts | Nein |
comment:beforeCreate | Bevor Kommentar gespeichert wird | Kommentar, kann abbrechen | Nein |
comment:moderate | Freigabestatus des Kommentars festlegen | Status | Ja |
comment:afterCreate | Nach dem Speichern eines Kommentars | Nichts | Nein |
comment:afterModerate | Nach Änderung des Kommentarstatus durch Admin | Nichts | Nein |
page:metadata | Rendern des öffentlichen Seiten-Heads | Tags beisteuern | Nein |
page:fragments | Rendern des öffentlichen Seiten-Body | Skripte einfügen | Nein |
plugin:install | Bei Erstinstallation des Plugins | Nichts | Nein |
plugin:activate | Wenn Plugin aktiviert wird | Nichts | Nein |
plugin:deactivate | Wenn Plugin deaktiviert wird | Nichts | Nein |
plugin:uninstall | Wenn Plugin entfernt wird | Nichts | Nein |
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
voidzurü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
falsezurück, um die Löschung abzubrechen - Geben Sie
trueodervoidzurü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
voidzurü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
falsezurü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
falsezurück, um abzulehnen - Geben Sie
voidzurü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
| Option | Typ | Standard | Beschreibung |
|---|---|---|---|
priority | number | 100 | Ausführungsreihenfolge (niedriger = früher) |
timeout | number | 5000 | Maximale Ausführungszeit in Millisekunden |
dependencies | string[] | [] | Plugin-IDs, die zuerst laufen müssen |
errorPolicy | string | "abort" | "continue", um Fehler zu ignorieren |
exclusive | boolean | false | Nur 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:
- Sortiert nach
priority(aufsteigend) - Plugins mit
dependencieslaufen nach ihren Abhängigkeiten - 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: ... }