フックを使うと、プラグインはイベントに応じてコードを実行できます。すべてのフックはイベントオブジェクトとプラグインコンテキストを受け取り、プラグインの定義時に宣言します。実行時の動的な登録はありません。
このページでは sandboxed プラグインを扱います。ネイティブプラグインも同じフック名とイベント型を使いますが、インプロセスのフックパイプラインを使い、さらに page:fragments を登録できます。sandbox での保存拒否と、隔離ランナーでの失敗時の動作は後述します。
フックシグネチャ
すべてのフックハンドラーは 2 つの引数を取ります。
async (event, ctx) => ReturnType;
event— 直前に起きたこと(保存中のコンテンツ、アップロードされたメディア、ライフサイクルの遷移など)に関するデータctx— ストレージ、KV、ログ、capability で制御される API を備えたPluginContext
定義を SandboxedPlugin 型の定数に代入すると、event はフック名から推論され(完全な正規のイベント型)、ctx は PluginContext と推論されるため、ハンドラーにパラメータの型注釈は不要です。その定数をデフォルトエクスポートしてください。ヘルパー内でイベント型を名前で参照するには、emdash/plugin からインポートします。
フック設定
フックは、ハンドラー単体として宣言することも、設定オブジェクトで包んで宣言することもできます。プラグインが意図的なインプロセス実行にも対応し、以下で説明するメタデータが必要な場合を除き、ハンドラー単体の形式を推奨します。
シンプル
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved");
},
}, 完全な設定
hooks: {
"content:afterSave": {
priority: 100,
timeout: 5000,
handler: async (event, ctx) => {
ctx.log.info("Content saved");
},
},
}, 設定オプション
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
priority | number | 100 | 実行順序。数値が小さいほど先に実行されます。 |
timeout | number | 5000 | 最大実行時間(ミリ秒)。 |
exclusive | boolean | false | アクティブなプロバイダーになれるプラグインは 1 つだけです。email:deliver と comment:moderate で使用します。 |
handler | function | — | フックハンドラー関数。必須です。 |
必要な capability
一部のフックは保護されたデータを公開したり、操作を変更したりできます。EmDash は、マニフェストが対応する capability を宣言している場合にのみ、それらを登録します。
| フック | Capability | 理由 |
|---|---|---|
content:beforeSave | content:write | このフックは送信されたコンテンツを置き換えられます。 |
content:beforePublish、content:beforeSchedule、content:beforeUnpublish | hooks.content-policy:register | これらのフックは公開状態の変更を拒否できます。 |
その他の content:* フック | content:read | イベントがコンテンツを公開するか、エントリを特定します。 |
media:beforeUpload | media:write | このフックはアップロードのメタデータを置き換えたり、アップロードを停止したりできます。 |
media:afterUpload | media:read | イベントが保存されたメディア項目を公開します。 |
email:beforeSend、email:afterSend | hooks.email-events:register | これらのフックはメールのライフサイクルイベントを検査します。 |
email:deliver | hooks.email-transport:register | このフックはメールトランスポートプロバイダーになります。 |
すべての comment:* フック | users:read | コメントイベントには、投稿者の連絡先情報とリクエストのメタデータが含まれる場合があります。 |
page:fragments | hooks.page-fragments:register | このフックはファーストパーティのページコンテンツを注入し、ネイティブ専用です。 |
ライフサイクルフック、cron、page:metadata には登録用の capability がありません。フックがイベントを読み取るだけで、対応する ctx API を呼び出さない場合でも、一覧にある capability を宣言してください。この宣言により、オペレーターに正確な同意プロンプトが表示され、ctx API が制御され、プラグインがインプロセスで実行されるときに必須となります。実行時の影響については Capability とセキュリティ で説明しています。
ライフサイクルフック
プラグインのインストール、有効化、無効化、削除の際に実行されます。
plugin:install
プラグインがサイトに最初に追加されたときに 1 回だけ実行されます。
この例は、マニフェストが items ストレージコレクションを宣言していることを前提としています。
"plugin:install": async (_event, ctx) => {
ctx.log.info("Installing plugin...");
await ctx.settings.set("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");
},
イベント: {} — 戻り値: Promise<void>
plugin:uninstall
プラグインがサイトから削除されたときに実行されます。
"plugin:uninstall": async (event, ctx) => {
ctx.log.info("Uninstalling plugin...");
if (event.deleteData) {
while (true) {
const result = await ctx.storage.items.query({ limit: 100 });
if (result.items.length === 0) break;
await ctx.storage.items.deleteMany(result.items.map((item) => item.id));
}
}
},
イベント: { deleteData: boolean } — 戻り値: Promise<void>
コンテンツフック
サイトコンテンツの作成、更新、削除の操作中に実行されます。
content:beforeSave
コンテンツが保存される前に実行されます。変更したコンテンツ、sandbox フックのエラー結果を返すか、変更しない場合は void を返します。
sandbox から保存を拒否するには、SAVE_REJECTED エラーを持つバージョン付きのフック結果を返します。reason には 1〜500 文字のプレーンテキストを設定してください。EmDash はプラグインを特定し、その理由を編集者に表示します。空、長すぎる、不正な形式、未知のエラー結果は、汎用のフックエラーとして保存を失敗させます。
"content:beforeSave": async (event, ctx) => {
const { content } = event;
if (typeof content.title !== "string" || content.title.trim() === "") {
return {
__emdashSandboxHookResult: true,
version: 1,
error: {
code: "SAVE_REJECTED",
reason: "Add a title before saving.",
},
};
}
content.title = content.title.trim();
return content;
},
reason に HTML を入れないでください。管理画面はこの値をテキストとして表示します。
ホストプロセスからは、代わりに ContentSaveRejectedError(emdash からエクスポートされています)をスローします。API はあなたのメッセージとともに SAVE_REJECTED を返します。どちらの実行モードでも、それ以外の例外は汎用の CONTENT_HOOK_ERROR レスポンスとして保存を失敗させます。
イベント: { content, collection, isNew, id, actor } — 戻り値: 変更したコンテンツ、sandbox フックエラー結果、または void。更新時、id は既存項目の ID で、content は送信されたフィールド値のみを持ちます。保存済み項目は ctx.content.get(event.collection, event.id) で読み込みます。エントリの slug は content に含まれず、フックが返した slug キーは未知のフィールドとして検証に失敗します。認証済みの REST、ビジュアル編集、MCP 保存には actor.id と数値の actor.role が含まれます。認証ユーザーなしの内部書き込みは actor を省略します。
content:afterSave
コンテンツが正常に保存された後に実行されます。通知、ログ記録、外部への同期といった副作用に使います。
"content:afterSave": async (event, ctx) => {
const contentId = String(event.content.id);
ctx.log.info(`${event.isNew ? "Created" : "Updated"} ${event.collection}/${contentId}`, {
actorId: event.actor?.id,
});
if (ctx.http) {
await ctx.http.fetch("https://api.example.com/webhook", {
method: "POST",
body: JSON.stringify({ event: "content:save", id: contentId }),
});
}
},
イベント: { content, collection, isNew, actor } — 戻り値: Promise<void>。認証済み保存には content:beforeSave と同じ任意の actor スナップショットが含まれます。
content:beforeDelete
コンテンツが削除される前に実行されます。false を返すとキャンセルされ、true または void を返すと許可されます。
"content:beforeDelete": async (event, ctx) => {
if (event.collection === "pages" && event.id === "home") {
ctx.log.warn("Cannot delete home page");
return false;
}
return true;
},
イベント: { id, collection, permanent: false } — 戻り値: boolean | void
このフックは、エントリがゴミ箱に移動される前に実行されます。ゴミ箱からエントリを恒久的に削除するときには、content:beforeDelete は再度実行されません。
content:afterDelete
コンテンツが正常に削除された後に実行されます。
"content:afterDelete": async (event, ctx) => {
await ctx.storage.cache.delete(`${event.collection}:${event.id}`);
},
イベント: { id, collection, permanent } — 戻り値: Promise<void>。permanent はエントリがゴミ箱に移されたとき false、恒久削除されたとき true です。
hooks.content-policy:register を宣言すると、コンテンツの読み取り、書き込み、公開操作へのアクセス権を得ることなく、公開、スケジュール、非公開を検査して拒否できます。
操作を許可するには void を返し、拒否するには { cancel: true, reason } を返します。理由は 1〜500 文字のプレーンテキストでなければなりません。無効な判断や予期しないエラーは、例外を公開せずにデフォルトで中止します。明示的な拒否は PUBLISH_REJECTED、SCHEDULE_REJECTED、UNPUBLISH_REJECTED を返します。
3 つのイベントはいずれも { content, collection, origin, actor? } を含みます。origin.source は api、mcp、visual-editor、plugin、scheduler、system のいずれかで、プラグインの origin には pluginId も含まれます。認証済みの人間による操作には、actor.id、数値の actor.role、対応する actor.source が含まれます。EmDash が visual-editor の origin を受け入れるのは、認証済みツールバーのレンダリングに埋め込まれた、署名付きで有効期間の短いアクショントークンからのみです。通常の API リクエストは origin を選択できません。
公開およびスケジュールのイベントでは、有効なドラフトを content.data で、ステージされた slug を content.slug で公開します。非公開のイベントでは、その操作によって削除される現在公開中のコンテンツを公開します。
content:beforePublish
次のフックは、コンテンツを公開する前に承認マーカーを要求します。
"content:beforePublish": async (event) => {
const data = event.content.data;
const approvalStatus =
typeof data === "object" && data !== null && "approval_status" in data
? data.approval_status
: undefined;
if (approvalStatus !== "approved") {
return { cancel: true, reason: "Approve this entry before publishing." };
}
},
このフックは、手動、MCP、プラグイン、システム、スケジュールによる公開の前に実行されます。スケジュールされたコンテンツは、公開時刻が来たときに再度チェックされます。スケジューラーによる拒否はエントリのスケジュールを解除し、公開しても問題のない理由を保存し、影響を受けたエントリをダッシュボードに一覧表示します。スケジューラーの実行のたびに同じ恒久的な拒否を再試行することはありません。スケジュール、公開、削除のいずれかが成功すると、その記録は消去されます。エントリまたはポリシープラグインが利用できなくなった場合、管理者は古い記録を閉じることができます。
content:beforeSchedule
エントリに公開時刻が設定される前に実行されます。イベントには scheduledAt も含まれます。
content:beforeUnschedule フックはありません。管理者は常に将来の公開を取り消せます。
content:beforeUnpublish
公開中のコンテンツが削除される前に実行されます。
content:afterPublish
コンテンツがドラフトから公開状態に昇格された後に実行されます。content:read capability が必要です。
イベント: { content, collection } — 戻り値: Promise<void>
content:afterUnpublish
コンテンツが公開状態からドラフトに戻された後に実行されます。content:read capability が必要です。
イベント: { content, collection } — 戻り値: Promise<void>
content:afterRestore
ゴミ箱のコンテンツが復元された後に実行されます。content:read capability が必要です。
イベント: { content, collection } — 戻り値: Promise<void>
content:afterSchedule
コンテンツが将来の公開に向けてスケジュールされた後に実行されます。content:read capability が必要です。
イベント: { content, collection } — 戻り値: Promise<void>
content:afterUnschedule
スケジュールされたコンテンツのスケジュールが解除された後に実行されます。content:read capability が必要です。
イベント: { content, collection } — 戻り値: Promise<void>
メディアフック
media:beforeUpload
ファイルがアップロードされる前に実行されます。変更したファイルのメタデータを返すか、スローしてキャンセルします。
"media:beforeUpload": async (event, ctx) => {
if (!event.file.type.startsWith("image/")) {
throw new Error("Only images are allowed");
}
if (event.file.size > 10 * 1024 * 1024) {
throw new Error("File too large");
}
return { ...event.file, name: `${Date.now()}-${event.file.name}` };
},
イベント: { file: { name, type, size } } — 戻り値: 変更したファイルまたは void
media:afterUpload
ファイルが正常にアップロードされた後に実行されます。
イベント: { media: { id, filename, mimeType, size, url, createdAt } } — 戻り値: Promise<void>
公開ページフック
これらのフックを使うと、プラグインはレンダリングされた公開ページに内容を追加できます。テンプレートは、emdash/ui の <EmDashHead>、<EmDashBodyStart>、<EmDashBodyEnd> コンポーネントを含めることで有効にします。
page:metadata
<head> に型付きのメタデータ(meta タグ、OpenGraph プロパティ、許可リストに載った <link> の rel、JSON-LD)を追加します。sandboxed プラグインとネイティブプラグインの両方で利用できます。 コアが追加内容を検証し、重複を排除してレンダリングします。プラグインが返すのは構造化データであり、生の 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 };
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;
}
}
戻り値: PageMetadataContribution | PageMetadataContribution[] | null
追加内容の種類:
| 種類 | レンダリング結果 | 重複排除キー |
|---|---|---|
meta | <meta name="..." content="..."> | key または name |
property | <meta property="..." content="..."> | key または property |
link | <link rel="<allowed value>" href="..."> | canonical: 単一、alternate: key または hreflang |
jsonld | <script type="application/ld+json"> | id(存在する場合) |
どの重複排除キーでも、最初の追加内容が優先されます。<EmDashHead> は、プラグイン → サイト設定 → テンプレートが提供するベースメタデータの順に追加内容を組み合わせるため、プラグインの追加内容はその下のすべてを上書きします。コンテンツページでは、ベースメタデータが生成される前に、エントリの SEO パネルの値がページコンテキストに取り込まれます。これらはテンプレートが提供するフィールドを置き換え(フックがページコンテキストで目にするのもこの値です)、プラグインの追加内容は先勝ちの重複排除によって引き続き優先されます。リンクの rel は、セキュリティ上固定された許可リスト(canonical、alternate、author、license、nlweb、site.standard.document)に制限され、href は HTTP または HTTPS でなければなりません。
page:fragments
ページの挿入ポイントに、生の HTML、スクリプト、スタイルシートを追加します。ネイティブプラグイン専用です。
sandboxed プラグインはこのフックを使えません。出力がファーストパーティのコードとして訪問者のブラウザーで実行され、sandbox の境界の外に出るためです。sandbox で安全にページへ追加するには、page:metadata を使ってください。この機能が必要な場合は、ネイティブプラグイン: ページフラグメント を参照してください。
フック実行順
sandboxed 形式のプラグインがインプロセスで実行される場合、フックは共有のフックパイプラインを使います。
priorityの値が小さいフックが先に実行されます。- 優先度が同じ場合、フックはプラグインの登録順に実行されます。
dependenciesを持つフックは、それらのプラグインの完了を待ちます。
// Plugin A
"content:afterSave": { priority: 50, handler: async () => {} }
// Plugin B
"content:afterSave": { priority: 100, handler: async () => {} }
// Plugin C
"content:afterSave": {
priority: 200,
dependencies: ["plugin-a"], // waits for A even if its priority would normally be later
handler: async () => {},
}
隔離された sandbox ランナーは、アクティブな sandboxed プラグインをロード順に呼び出します。フックは互いに独立させてください。ある sandboxed プラグインが別のプラグインより先に実行されることを前提にしないでください。
エラー処理
sandboxed フックの失敗時の動作は、フックが実行されるタイミングによって異なります。
content:beforeSaveでスローされたエラーは、CONTENT_HOOK_ERRORとして保存を失敗させます。編集者に具体的な検証の理由を表示したい場合は、ドキュメント化されているSAVE_REJECTEDエンベロープを返してください。content:beforeDeleteからfalseを返すと、ゴミ箱への移動が止まります。このフックがスローした場合、EmDash はエラーをログに記録し、削除を続行します。- コンテンツの after フックは、操作が成功した後に実行されます。そのエラーはログに記録されるだけで、操作をロールバックすることはできません。
- ライフサイクル、メディア、メール、コメントのフックは、元となる操作の契約に従います。失敗時の動作に依存する前に、フックリファレンス で個々の戻り値を確認してください。
インプロセスのプラグインは、完全な設定形式で errorPolicy: "abort" または "continue" を使用できます。この設定は、隔離された sandboxed プラグインに対する移植可能な回復手段ではありません。
タイムアウト
インプロセスのフックパイプラインのデフォルトは 5,000 ms で、完全な設定形式ではより長い timeout を指定できます。
"content:afterSave": {
timeout: 30000,
handler: async (event, ctx) => {
// Long-running operation
},
},
フックリファレンス
| フック | トリガー | 戻り値 | 排他 |
|---|---|---|---|
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 | いいえ |
content:afterRestore | コンテンツ復元後 | void | いいえ |
content:afterSchedule | コンテンツのスケジュール後 | void | いいえ |
content:afterUnschedule | コンテンツのスケジュール解除後 | void | いいえ |
media:beforeUpload | ファイルアップロード前 | 変更したファイル情報または void | いいえ |
media:afterUpload | ファイルアップロード後 | void | いいえ |
cron | スケジュールタスクの発火 | void | いいえ |
email:beforeSend | メール配信前 | 変更したメッセージ、false、または void | いいえ |
email:deliver | トランスポート経由のメール配信 | void | はい |
email:afterSend | メール配信後 | void | いいえ |
comment:beforeCreate | コメント保存前 | 変更したイベント、false、または void | いいえ |
comment:moderate | コメントのステータスの決定 | { status, reason? } | はい |
comment:afterCreate | コメント保存後 | void | いいえ |
comment:afterModerate | 管理者によるコメントのステータス変更 | void | いいえ |
byline:afterSave | Byline 保存後 | void | いいえ |
byline:afterDelete | Byline 削除後 | void | いいえ |
page:metadata | ページのレンダリング | 追加内容または null | いいえ |
page:fragments | ページのレンダリング(ネイティブ専用) | 追加内容または null | いいえ |
完全なイベント型とハンドラーシグネチャについては、フックリファレンス を参照してください。