フックにより、プラグインはイベントに応じてコードを実行できます。すべてのフックはイベントオブジェクトとプラグインコンテキストを受け取り、プラグイン定義時に宣言されます。ランタイムでの動的登録はありません。
このページはサンドボックス化されたプラグインを扱います。ネイティブプラグインは同じフック名とイベント型を使いますが、インプロセスのフックパイプラインを使い、さらに page:fragments を登録できます。サンドボックスの保存拒否と孤立ランナーの失敗動作は以下で説明します。
フックシグネチャ
すべてのフックハンドラーは 2 つの引数を取ります:
async (event, ctx) => ReturnType;
event— 直前に起きたことについてのデータ(保存されるコンテンツ、アップロードされたメディア、ライフサイクル遷移など)ctx— ストレージ、KV、ログ、capability でゲートされた API を持つPluginContext
定義を SandboxedPlugin 型の定数に割り当てると、event はフック名から(完全な正規イベント型として)推論され、ctx は PluginContext になるため、ハンドラーにパラメータ注釈は不要です。その定数を default としてエクスポートします。ヘルパーでイベント型を名前で参照するには、emdash/plugin からインポートします。
フック設定
フックは素のハンドラーとして、または設定オブジェクトに包んで宣言できます。プラグインが意図的なインプロセス実行もサポートし、下記のメタデータが必要な場合を除き、素の形式を推奨します。
Simple
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved");
},
}, Full config
hooks: {
"content:afterSave": {
priority: 100,
timeout: 5000,
handler: async (event, ctx) => {
ctx.log.info("Content saved");
},
},
}, 設定オプション
| Option | Type | Default | Description |
|---|---|---|---|
priority | number | 100 | 実行順。数値が小さいほど先に実行。 |
timeout | number | 5000 | 最大実行時間(ミリ秒)。 |
exclusive | boolean | false | アクティブなプロバイダーは 1 つのプラグインのみ。email:deliver と comment:moderate で使用。 |
handler | function | — | フックハンドラー関数。必須。 |
必要な capability
いくつかのフックは保護されたデータを公開したり、操作を変更したりできます。EmDash はマニフェストが一致する capability を宣言した場合にのみ登録します:
| Hooks | Capability | Reason |
|---|---|---|
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 をゲートし、プラグインがインプロセスで動くときに必要です。Capabilities とセキュリティ がランタイム効果を説明します。
ライフサイクルフック
プラグインのインストール、有効化、無効化、削除時に実行されます。
plugin:install
プラグインがサイトに初めて追加されたときに一度実行されます。
この例はマニフェストが 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" });
},
Event: {} — Returns: Promise<void>
plugin:activate
プラグインが有効化されるとき(インストール後または再有効化時)に実行されます。
"plugin:activate": async (_event, ctx) => {
ctx.log.info("Plugin activated");
},
Event: {} — Returns: Promise<void>
plugin:deactivate
プラグインが無効化されるとき(削除はされない)に実行されます。
"plugin:deactivate": async (_event, ctx) => {
ctx.log.info("Plugin deactivated");
},
Event: {} — Returns: 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));
}
}
},
Event: { deleteData: boolean } — Returns: Promise<void>
コンテンツフック
サイトコンテンツの作成、更新、削除操作中に実行されます。
content:beforeSave
コンテンツが保存される前に実行されます。変更したコンテンツ、サンドボックスフックエラー結果、または変更なしの void を返します。
サンドボックスから保存を拒否するには、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.",
},
};
}
if (typeof content.slug === "string") {
content.slug = content.slug.toLowerCase().replace(/\s+/g, "-");
}
return content;
},
reason に HTML を入れないでください。管理画面は値をテキストとしてレンダリングします。
ホストプロセスからは代わりに ContentSaveRejectedError(emdash からエクスポート)を throw します。API はメッセージ付きの SAVE_REJECTED を返します。どちらの実行モードからの他の例外も、汎用の CONTENT_HOOK_ERROR 応答で保存を失敗させます。
Event: { content, collection, isNew, id, actor } — Returns: 変更したコンテンツ、サンドボックスフックエラー結果、または void。更新時、id は既存項目の ID で、content は送信されたフィールド値のみを持ちます。保存済み項目は ctx.content.get(event.collection, event.id) で読み込みます。認証済みの 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 }),
});
}
},
Event: { content, collection, isNew, actor } — Returns: 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;
},
Event: { id, collection, permanent: false } — Returns: boolean | void
このフックはエントリがゴミ箱に移される前に実行されます。ゴミ箱からエントリを恒久削除しても content:beforeDelete は再実行されません。
content:afterDelete
コンテンツが正常に削除された後に実行されます。
"content:afterDelete": async (event, ctx) => {
await ctx.storage.cache.delete(`${event.collection}:${event.id}`);
},
Event: { id, collection, permanent } — Returns: 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 に、ステージされたスラッグを 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 が必要です。
Event: { content, collection } — Returns: Promise<void>
content:afterUnpublish
コンテンツがライブから下書きに戻された後に実行されます。content:read capability が必要です。
Event: { content, collection } — Returns: Promise<void>
content:afterRestore
ゴミ箱のコンテンツが復元された後に実行されます。content:read capability が必要です。
Event: { content, collection } — Returns: Promise<void>
content:afterSchedule
コンテンツが将来の公開のためにスケジュールされた後に実行されます。content:read capability が必要です。
Event: { content, collection } — Returns: Promise<void>
content:afterUnschedule
スケジュール済みコンテンツのスケジュールが解除された後に実行されます。content:read capability が必要です。
Event: { content, collection } — Returns: Promise<void>
メディアフック
media:beforeUpload
ファイルがアップロードされる前に実行されます。変更したファイルメタデータを返すか、キャンセルするために throw します。
"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}` };
},
Event: { file: { name, type, size } } — Returns: 変更したファイルまたは void
media:afterUpload
ファイルが正常にアップロードされた後に実行されます。
Event: { media: { id, filename, mimeType, size, url, createdAt } } — Returns: Promise<void>
公開ページフック
これらにより、プラグインはレンダリングされた公開ページに貢献できます。テンプレートは emdash/ui の <EmDashHead>、<EmDashBodyStart>、<EmDashBodyEnd> コンポーネントを含めることでオプトインします。
page:metadata
型付きメタデータを <head> に貢献します — メタタグ、OpenGraph プロパティ、許可リストの <link> rel、JSON-LD。サンドボックスとネイティブの両方のプラグインで利用可能。 コアが貢献を検証、重複排除、レンダリングします。プラグインは構造化データを返し、生の 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,
},
};
},
Event:
{
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;
}
}
Returns: PageMetadataContribution | PageMetadataContribution[] | null
Contribution kinds:
| Kind | Renders | Dedupe key |
|---|---|---|
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 パネル値がベースメタデータ生成前にページコンテキストに折り込まれます。テンプレート提供のフィールドを置き換え(フックがページコンテキストで見るものになります)、プラグイン貢献は first-wins 重複排除でなお勝ちます。リンクの rel はセキュリティロックされた許可リスト(canonical、alternate、author、license、nlweb、site.standard.document)に制限されます。href は HTTP または HTTPS である必要があります。
page:fragments
生の HTML、スクリプト、スタイルシートをページ挿入点に貢献します。ネイティブプラグインのみ。
サンドボックス化されたプラグインはこのフックを使えません。出力が訪問者のブラウザでファーストパーティコードとして、サンドボックス境界の外で実行されるためです。サンドボックス安全なページ貢献には page:metadata を使います。この面が必要な場合は ネイティブプラグイン: ページフラグメント を参照してください。
フック実行順
サンドボックス形式のプラグインがインプロセスで動くとき、フックは共有フックパイプラインを使います:
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 () => {},
}
孤立サンドボックスランナーは、アクティブなサンドボックス化プラグインをロード順で呼び出します。フックを独立させてください。あるサンドボックス化プラグインが別の前に実行されることを要求しないでください。
エラー処理
サンドボックス化フックの失敗は、フックがいつ実行されるかに依存します:
- throw された
content:beforeSaveエラーはCONTENT_HOOK_ERRORで保存を失敗させます。編集者が特定の検証理由を見るべきときは、文書化されたSAVE_REJECTEDエンベロープを返します。 content:beforeDeleteからfalseを返すとゴミ箱への移動が止まります。そのフックが throw すると、EmDash はエラーをログし、削除を続行します。- コンテンツの after フックは操作成功後に実行されます。そのエラーはログされ、操作をロールバックできません。
- ライフサイクル、メディア、メール、コメントフックは、発生元の操作の契約に従います。失敗動作に依存する前に フックリファレンス で特定の戻り値を確認してください。
インプロセスプラグインは完全設定形式で errorPolicy: "abort" または "continue" を使えます。その設定は孤立サンドボックス化プラグイン向けの移植可能な復旧制御ではありません。
タイムアウト
インプロセスフックパイプラインのデフォルトは 5,000 ms で、完全設定形式でより長い timeout を受け付けます:
"content:afterSave": {
timeout: 30000,
handler: async (event, ctx) => {
// Long-running operation
},
},
フックリファレンス
| Hook | Trigger | Return | Exclusive |
|---|---|---|---|
plugin:install | 初回プラグインインストール | void | No |
plugin:activate | プラグイン有効化 | void | No |
plugin:deactivate | プラグイン無効化 | void | No |
plugin:uninstall | プラグイン削除 | void | No |
content:beforeSave | コンテンツ保存前 | 変更コンテンツ、拒否エンベロープ、または void | No |
content:afterSave | コンテンツ保存後 | void | No |
content:beforeDelete | ゴミ箱移動前 | キャンセルは false、それ以外は許可 | No |
content:afterDelete | ゴミ箱または恒久削除後 | void | No |
content:afterPublish | コンテンツ公開後 | void | No |
content:afterUnpublish | コンテンツ非公開後 | void | No |
content:afterRestore | コンテンツ復元後 | void | No |
content:afterSchedule | コンテンツスケジュール後 | void | No |
content:afterUnschedule | コンテンツスケジュール解除後 | void | No |
media:beforeUpload | ファイルアップロード前 | 変更ファイル情報または void | No |
media:afterUpload | ファイルアップロード後 | void | No |
cron | スケジュールタスク発火 | void | No |
email:beforeSend | メール配信前 | 変更メッセージ、false、または void | No |
email:deliver | 転送経由でメール配信 | void | Yes |
email:afterSend | メール配信後 | void | No |
comment:beforeCreate | コメント保存前 | 変更イベント、false、または void | No |
comment:moderate | コメント状態を決定 | { status, reason? } | Yes |
comment:afterCreate | コメント保存後 | void | No |
comment:afterModerate | 管理者がコメント状態を変更 | void | No |
page:metadata | ページレンダー | 貢献または null | No |
page:fragments | ページレンダー(ネイティブのみ) | 貢献または null | No |
完全なイベント型とハンドラーシグネチャは フックリファレンス を参照してください。