フックにより、プラグインはイベントに応じてコードを実行できます。すべてのフックはイベントオブジェクトとプラグインコンテキストを受け取ります。フックはプラグイン定義時に宣言され、実行時に動的に登録されることはありません。
フックのシグネチャ
各フックハンドラーは 2 つの引数を受け取ります。
async (event: EventType, ctx: PluginContext) => ReturnType;
event— イベントに関するデータ(保存されるコンテンツ、アップロードされたメディアなど)ctx— ストレージ、KV、ログ、capability で制御された API を含むプラグインコンテキスト
フックの設定
フックは単純なハンドラーとして、または完全な設定付きで宣言できます。
シンプル
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved");
}
} 完全な設定
hooks: {
"content:afterSave": {
priority: 100,
timeout: 5000,
dependencies: ["audit-log"],
errorPolicy: "continue",
handler: async (event, ctx) => {
ctx.log.info("Content saved");
}
}
} 設定オプション
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
priority | number | 100 | 実行順。小さい値ほど先に実行されます。 |
timeout | number | 5000 | 最大実行時間(ミリ秒)。 |
dependencies | string[] | [] | このフックより前に実行される必要があるプラグイン ID。 |
errorPolicy | "abort" | "continue" | "abort" | エラー時にパイプラインを止めるか。 |
exclusive | boolean | false | アクティブなプロバイダーは 1 つのプラグインのみ。email:deliver と comment:moderate で使用。 |
handler | function | — | フックハンドラー関数。必須。 |
ライフサイクルフック
ライフサイクルフックは、プラグインのインストール、有効化、無効化の際に実行されます。
plugin:install
プラグインがサイトに初めて追加されたときに 1 回実行されます。
"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" });
}
イベント: {}
戻り値: Promise<void>
plugin:activate
プラグインが有効化されたときに実行されます(インストール後、または再有効化時)。
"plugin:activate": async (_event, ctx) => {
ctx.log.info("Plugin activated");
}
イベント: {}
戻り値: Promise<void>
plugin:deactivate
プラグインが無効化されたときに実行されます(削除はされません)。
"plugin:deactivate": async (_event, ctx) => {
ctx.log.info("Plugin deactivated");
// Release resources, pause background work
}
イベント: {}
戻り値: Promise<void>
plugin:uninstall
プラグインがサイトから削除されたときに実行されます。
"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));
}
}
イベント: { deleteData: boolean }
戻り値: Promise<void>
コンテンツフック
コンテンツフックは作成、更新、削除の操作中に実行されます。
content:beforeSave
コンテンツが保存される前に実行されます。変更後のコンテンツを返すか、void で変更なし。例外を投げると保存をキャンセルします。
"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;
}
イベント:
{
content: Record<string, unknown>; // Content data being saved
collection: string; // Collection name
isNew: boolean; // True if creating, false if updating
}
戻り値: Promise<Record<string, unknown> | void>
content:afterSave
コンテンツの保存が成功した後に実行されます。通知、ログ、外部システムとの同期などの副作用に使います。
"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 })
});
}
}
イベント:
{
content: Record<string, unknown>; // Saved content (includes id, timestamps)
collection: string;
isNew: boolean;
}
戻り値: Promise<void>
content:beforeDelete
コンテンツが削除される前に実行されます。削除をキャンセルするには false、許可するには true または void を返します。
"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;
}
イベント:
{
id: string; // Content ID being deleted
collection: string;
}
戻り値: Promise<boolean | void>
content:afterDelete
コンテンツの削除が成功した後に実行されます。
"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}`);
}
イベント:
{
id: string;
collection: string;
}
戻り値: Promise<void>
content:afterPublish
コンテンツが公開された後(ドラフトから本番へ昇格)に実行されます。キャッシュ無効化、通知、外部システムとの同期などの副作用に使います。
read:content capability が必要です。
"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 })
});
}
}
イベント:
{
content: Record<string, unknown>; // Published content (includes id, timestamps)
collection: string;
}
戻り値: Promise<void>
content:afterUnpublish
コンテンツが非公開になった後(本番からドラフトへ戻す)に実行されます。キャッシュ無効化や外部システムへの通知などの副作用に使います。
read:content capability が必要です。
"content:afterUnpublish": async (event, ctx) => {
const { content, collection } = event;
ctx.log.info(`Unpublished ${collection}/${content.id}`);
}
イベント:
{
content: Record<string, unknown>; // Unpublished content
collection: string;
}
戻り値: Promise<void>
メディアフック
メディアフックはファイルアップロード中に実行されます。
media:beforeUpload
ファイルがアップロードされる前に実行されます。変更後のファイル情報を返すか、void でそのまま。例外を投げるとアップロードをキャンセルします。
"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}`
};
}
イベント:
{
file: {
name: string; // Original filename
type: string; // MIME type
size: number; // Size in bytes
}
}
戻り値: Promise<{ name: string; type: string; size: number } | void>
media:afterUpload
ファイルのアップロードが成功した後に実行されます。
"media:afterUpload": async (event, ctx) => {
const { media } = event;
ctx.log.info(`Uploaded ${media.filename}`, {
id: media.id,
size: media.size,
mimeType: media.mimeType
});
}
イベント:
{
media: {
id: string;
filename: string;
mimeType: string;
size: number | null;
url: string;
createdAt: string;
}
}
戻り値: Promise<void>
フックの実行順序
フックは次の順序で実行されます。
priorityの値が小さいフックが先に実行される- 同じ優先度では、プラグイン登録順で実行される
dependenciesがあるフックは、指定したプラグインの完了を待つ
// 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 () => {}
}
エラー処理
フックが例外を投げたりタイムアウトした場合:
errorPolicy: "abort"— パイプライン全体が停止します。元の操作が失敗する場合があります。errorPolicy: "continue"— エラーはログに記録され、残りのフックは実行されます。
"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");
}
}
タイムアウト
フックのデフォルトタイムアウトは 5000ms(5 秒)です。時間がかかる操作には増やしてください。
"content:afterSave": {
timeout: 30000, // 30 seconds
handler: async (event, ctx) => {
// Long-running operation
}
}
公開ページのフック
公開ページのフックにより、プラグインはレンダリングされたページの <head> と <body> に寄与できます。テンプレートは emdash/ui の <EmDashHead>、<EmDashBodyStart>、<EmDashBodyEnd> コンポーネントでオプトインします。
page:metadata
<head> に型付きメタデータを寄与します — meta タグ、OpenGraph プロパティ、canonical/alternate リンク、JSON-LD 構造化データ。trusted モードと sandbox モードの両方で動作します。
コアが寄与を検証・重複排除・レンダリングします。プラグインは生の 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,
},
};
}
イベント:
{
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 };
}
}
戻り値: PageMetadataContribution | PageMetadataContribution[] | null
寄与の種類:
| Kind | レンダリング | 重複排除キー |
|---|---|---|
meta | <meta name="..." content="..."> | key or name |
property | <meta property="..." content="..."> | key or property |
link | <link rel="canonical|alternate" href="..."> | canonical: singleton; alternate: key or hreflang |
jsonld | <script type="application/ld+json"> | id (if present) |
各重複排除キーでは最初の寄与が採用されます。リンクの href は HTTP または HTTPS である必要があります。
page:fragments
ページの挿入位置に生の HTML、スクリプト、マークアップを寄与します。trusted プラグインのみ — sandbox 化されたプラグインはこのフックを使えません。
"page:fragments": async (event, ctx) => {
return {
kind: "external-script",
placement: "head",
src: "https://www.googletagmanager.com/gtm.js?id=GTM-XXXXX",
async: true,
};
}
戻り値: PageFragmentContribution | PageFragmentContribution[] | null
配置: "head"、"body:start"、"body:end"。配置用コンポーネントを省略したテンプレートは、その配置を対象とする寄与を黙って無視します。
フック一覧
| Hook | トリガー | 戻り値 | 排他 |
|---|---|---|---|
plugin:install | 初回プラグインインストール | void | いいえ |
plugin:activate | プラグイン有効化 | void | いいえ |
plugin:deactivate | プラグイン無効化 | void | いいえ |
plugin:uninstall | プラグイン削除 | void | いいえ |
content:beforeSave | コンテンツ保存前 | 変更後のコンテンツまたは void | いいえ |
content:afterSave | コンテンツ保存後 | void | いいえ |
content:beforeDelete | コンテンツ削除前 | キャンセルは false、それ以外は許可 | いいえ |
content:afterDelete | コンテンツ削除後 | void | いいえ |
content:afterPublish | コンテンツ公開後 | void | いいえ |
content:afterUnpublish | コンテンツ非公開後 | void | いいえ |
media:beforeUpload | ファイルアップロード前 | 変更後のファイル情報または void | いいえ |
media:afterUpload | ファイルアップロード後 | void | いいえ |
cron | スケジュールタスク発火 | void | いいえ |
email:beforeSend | メール送信前 | 変更後のメッセージ、false、または void | いいえ |
email:deliver | transport 経由でメール配信 | void | はい |
email:afterSend | メール送信後 | void | いいえ |
comment:beforeCreate | コメント保存前 | 変更後のイベント、false、または void | いいえ |
comment:moderate | コメントステータスを決定 | { status, reason? } | はい |
comment:afterCreate | コメント保存後 | void | いいえ |
comment:afterModerate | 管理者がコメントステータスを変更 | void | いいえ |
page:metadata | ページレンダリング | 寄与または null | いいえ |
page:fragments | ページレンダリング(trusted) | 寄与または null | いいえ |
完全なイベント型とハンドラーシグネチャは フックリファレンス を参照してください。