フック

このページ

フックを使うと、プラグインはイベントに応じてコードを実行できます。すべてのフックはイベントオブジェクトとプラグインコンテキストを受け取り、プラグインの定義時に宣言します。実行時の動的な登録はありません。

このページでは 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");
		},
	},
},

設定オプション

オプション型デフォルト説明
prioritynumber100実行順序。数値が小さいほど先に実行されます。
timeoutnumber5000最大実行時間(ミリ秒)。
exclusivebooleanfalseアクティブなプロバイダーになれるプラグインは 1 つだけです。email:deliver と comment:moderate で使用します。
handlerfunction—フックハンドラー関数。必須です。

必要な capability

一部のフックは保護されたデータを公開したり、操作を変更したりできます。EmDash は、マニフェストが対応する capability を宣言している場合にのみ、それらを登録します。

フックCapability理由
content:beforeSavecontent:writeこのフックは送信されたコンテンツを置き換えられます。
content:beforePublish、content:beforeSchedule、content:beforeUnpublishhooks.content-policy:registerこれらのフックは公開状態の変更を拒否できます。
その他の content:* フックcontent:readイベントがコンテンツを公開するか、エントリを特定します。
media:beforeUploadmedia:writeこのフックはアップロードのメタデータを置き換えたり、アップロードを停止したりできます。
media:afterUploadmedia:readイベントが保存されたメディア項目を公開します。
email:beforeSend、email:afterSendhooks.email-events:registerこれらのフックはメールのライフサイクルイベントを検査します。
email:deliverhooks.email-transport:registerこのフックはメールトランスポートプロバイダーになります。
すべての comment:* フックusers:readコメントイベントには、投稿者の連絡先情報とリクエストのメタデータが含まれる場合があります。
page:fragmentshooks.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 形式のプラグインがインプロセスで実行される場合、フックは共有のフックパイプラインを使います。

  1. priority の値が小さいフックが先に実行されます。
  2. 優先度が同じ場合、フックはプラグインの登録順に実行されます。
  3. 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:afterSaveByline 保存後voidいいえ
byline:afterDeleteByline 削除後voidいいえ
page:metadataページのレンダリング追加内容または nullいいえ
page:fragmentsページのレンダリング(ネイティブ専用)追加内容または nullいいえ

完全なイベント型とハンドラーシグネチャについては、フックリファレンス を参照してください。