フック

このページ

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

このページはサンドボックス化されたプラグインを扱います。ネイティブプラグインは同じフック名とイベント型を使いますが、インプロセスのフックパイプラインを使い、さらに 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");
		},
	},
},

設定オプション

OptionTypeDefaultDescription
prioritynumber100実行順。数値が小さいほど先に実行。
timeoutnumber5000最大実行時間(ミリ秒)。
exclusivebooleanfalseアクティブなプロバイダーは 1 つのプラグインのみ。email:deliver と comment:moderate で使用。
handlerfunction—フックハンドラー関数。必須。

必要な capability

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

HooksCapabilityReason
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 をゲートし、プラグインがインプロセスで動くときに必要です。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:

KindRendersDedupe 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 を使います。この面が必要な場合は ネイティブプラグイン: ページフラグメント を参照してください。

フック実行順

サンドボックス形式のプラグインがインプロセスで動くとき、フックは共有フックパイプラインを使います:

  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 () => {},
}

孤立サンドボックスランナーは、アクティブなサンドボックス化プラグインをロード順で呼び出します。フックを独立させてください。あるサンドボックス化プラグインが別の前に実行されることを要求しないでください。

エラー処理

サンドボックス化フックの失敗は、フックがいつ実行されるかに依存します:

  • 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
	},
},

フックリファレンス

HookTriggerReturnExclusive
plugin:install初回プラグインインストールvoidNo
plugin:activateプラグイン有効化voidNo
plugin:deactivateプラグイン無効化voidNo
plugin:uninstallプラグイン削除voidNo
content:beforeSaveコンテンツ保存前変更コンテンツ、拒否エンベロープ、または voidNo
content:afterSaveコンテンツ保存後voidNo
content:beforeDeleteゴミ箱移動前キャンセルは false、それ以外は許可No
content:afterDeleteゴミ箱または恒久削除後voidNo
content:afterPublishコンテンツ公開後voidNo
content:afterUnpublishコンテンツ非公開後voidNo
content:afterRestoreコンテンツ復元後voidNo
content:afterScheduleコンテンツスケジュール後voidNo
content:afterUnscheduleコンテンツスケジュール解除後voidNo
media:beforeUploadファイルアップロード前変更ファイル情報または voidNo
media:afterUploadファイルアップロード後voidNo
cronスケジュールタスク発火voidNo
email:beforeSendメール配信前変更メッセージ、false、または voidNo
email:deliver転送経由でメール配信voidYes
email:afterSendメール配信後voidNo
comment:beforeCreateコメント保存前変更イベント、false、または voidNo
comment:moderateコメント状態を決定{ status, reason? }Yes
comment:afterCreateコメント保存後voidNo
comment:afterModerate管理者がコメント状態を変更voidNo
page:metadataページレンダー貢献または nullNo
page:fragmentsページレンダー(ネイティブのみ)貢献または nullNo

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