このサイトは非公式の日本語訳です。Cloudflare・EmDashプロジェクトが運営するサイトではありません。

このページで分かること

  • フックの書き方(イベントとプラグインのコンテキストを受け取る)と設定項目(prioritytimeoutexclusive
  • フックごとに必要な権限(Capability)と、ライフサイクル・コンテンツ・メディア・公開ページのフックの動き
  • 実行順、エラー時の扱い、タイムアウトと、すべてのフックの一覧
難易度
上級
読む時間
9分
このページの目次

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

このページでは、サンドボックス型プラグインについて説明します。ネイティブ型プラグインも同じフック名とイベントの型を使いますが、プロセス内のフックのパイプラインを使い、さらに page:fragments も登録できます。サンドボックス型での保存の拒否と、分離されたランナーでの失敗時の動きは、このあとで説明します。

フックのシグネチャー

すべてのフックのハンドラーは、2つの引数を受け取ります。

async (event, ctx) => ReturnType;
  • event:直前に起きたことのデータ(保存されようとしているコンテンツ、アップロードされたメディア、ライフサイクルの移行など)
  • ctx:ストレージ、KV、ログ出力、権限(Capability)で制限されるAPIを持つPluginContext

定義を SandboxedPlugin 型の定数に代入すると、event の型はフック名から(正式なイベントの型全体として)、ctx の型は PluginContext として推論されます。そのため、ハンドラーの引数に型注釈は必要ありません。その定数を既定のエクスポートとして書き出します。補助関数の中でイベントの型を名前で参照するには、emdash/plugin からインポートします。

本サイトの補足 やさしい解説

EmDashでは、プラグインの定義の hooks に、フック名とハンドラーをまとめて書きます。実行時にあとから登録する方法はありません。ハンドラーは、何が起きたかを表す event と、ストレージやログなどを使うための ctx の2つを受け取ります。SandboxedPlugin 型の定数に代入しておけば、この2つの型は自動で決まります。

フックの設定

フックは、ハンドラーだけの形で宣言することも、設定オブジェクトで包んで宣言することもできます。プラグインが意図的なプロセス内での実行にも対応していて、以下で説明するメタデータが必要な場合を除き、ハンドラーだけの形を使います。

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");
		},
	},
},

設定項目

項目 既定値 説明
priority number 100 実行順です。小さい数値が先に実行されます。
timeout number 5000 最大の実行時間(ミリ秒)です。
exclusive boolean false 有効な提供者を1つのプラグインだけに限ります。email:delivercomment:moderate で使います。
handler function フックのハンドラー関数です。必須です。

必要な権限

いくつかのフックは、保護されたデータを渡したり、処理を変更したりできます。EmDashは、マニフェストに対応する権限が宣言されている場合にだけ、これらのフックを登録します。

フック 権限 理由
content:beforeSave content:write 送信されたコンテンツを置き換えられるためです。
その他の content:* フック content:read イベントがコンテンツを渡すか、エントリーを特定するためです。
media:beforeUpload media:write アップロードのメタデータを置き換えたり、アップロードを止めたりできるためです。
media:afterUpload media:read イベントが保存されたメディアを渡すためです。
email:beforeSendemail:afterSend hooks.email-events:register メールのライフサイクルのイベントを調べるためです。
email:deliver hooks.email-transport:register フックがメールの送信手段(トランスポート)の提供者になるためです。
すべての comment:* フック users:read コメントのイベントに、投稿者の連絡先とリクエストのメタデータが含まれることがあるためです。
page:fragments hooks.page-fragments:register ファーストパーティのページのコンテンツを挿入するためで、ネイティブ型専用です。

ライフサイクルのフック、cronpage:metadata には、登録のための権限はありません。フックがイベントを読むだけで、対応する ctx のAPIを呼び出さない場合でも、表に記載した権限を宣言します。この宣言によって、運営者に正確な同意の確認を表示でき、ctx のAPIが制限されます。また、プラグインをプロセス内で動かす場合には、この宣言が必要です。実行時の効果は、権限(Capabilities)とセキュリティで説明しています。

本サイトの補足 やさしい解説

コンテンツを書き換えられるフックや、コンテンツ・メディア・コメントのデータを受け取るフックを使うには、マニフェストで対応する権限を宣言する必要があります。宣言がないと、そのフックは登録されません。たとえば、保存前にコンテンツを書き換えられる content:beforeSave には content:write が、保存後に実行される content:afterSave には content:read が必要です。

ライフサイクルのフック

プラグインのインストール、有効化、無効化、削除のときに実行されます。

plugin:install

プラグインが初めてサイトに追加されたときに、1回だけ実行されます。

この例では、マニフェストで items というストレージのコレクションを宣言していることを前提にしています。

"plugin:install": async (_event, ctx) => {
	ctx.log.info("Installing plugin...");
	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");
},

イベント: {} 戻り値: 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

コンテンツが保存される前に実行されます。変更したコンテンツ、サンドボックスのフックのエラー結果、またはコンテンツを変えない場合は 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を入れないでください。管理画面は、値をテキストとして描画します。

ホストのプロセスからは、代わりに ContentSaveRejectedErroremdash からエクスポートされています)をスローします。APIは、指定したメッセージとともに SAVE_REJECTED を返します。どちらの実行モードでも、それ以外の例外は汎用の CONTENT_HOOK_ERROR のレスポンスとして保存を失敗させます。

イベント: { content, collection, isNew, id, actor } 戻り値: 変更したコンテンツ、サンドボックスのフックのエラー結果、または 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 }),
		});
	}
},

イベント: { 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>。エントリーがゴミ箱に移された場合、permanentfalse です。完全に削除された場合は true です。

content:afterPublish

コンテンツが下書きから公開中の状態に移ったあとに実行されます。content:read の権限が必要です。

イベント: { content, collection } 戻り値: Promise<void>

content:afterUnpublish

コンテンツが公開中の状態から下書きに戻ったあとに実行されます。content:read の権限が必要です。

イベント: { content, collection } 戻り値: Promise<void>

content:afterRestore

ゴミ箱のコンテンツが元に戻されたあとに実行されます。content:read の権限が必要です。

イベント: { content, collection } 戻り値: Promise<void>

content:afterSchedule

コンテンツが将来の公開のために予約公開されたあとに実行されます。content:read の権限が必要です。

イベント: { content, collection } 戻り値: Promise<void>

content:afterUnschedule

予約公開したコンテンツの予約が解除されたあとに実行されます。content:read の権限が必要です。

イベント: { content, collection } 戻り値: Promise<void>

本サイトの補足 やさしい解説

保存前に実行される content:beforeSave は、コンテンツを書き換えたり、理由を示して保存を拒否したりできます。保存後に実行される content:afterSave は、通知や外部との同期に使い、保存そのものは取り消せません。削除は、ゴミ箱に移される時点で content:beforeDelete が実行され、ゴミ箱から完全に削除するときには再び実行されません。

メディアのフック

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

型付きのメタデータ(metaタグ、OpenGraphのプロパティ、許可リストにある <link> のrel、JSON-LD)を <head> に追加します。サンドボックス型とネイティブ型のどちらのプラグインでも使えます。 コアが追加された内容を検証し、重複を除き、描画します。プラグインは構造化されたデータを返し、生の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:1つだけ。alternate:key または hreflang
jsonld <script type="application/ld+json"> id(ある場合)

重複を判定するキーが同じ場合は、最初に追加されたものが採用されます。<EmDashHead> は、プラグイン → サイトの設定 → テンプレートが提供する基本のメタデータ、の順に内容を組み立てます。そのため、プラグインが追加した内容は、それより後ろのものすべてより優先されます。コンテンツのページでは、基本のメタデータが生成される前に、エントリーのSEOパネルの値がページのコンテキストに取り込まれます。これらの値は、テンプレートが提供するフィールドを置き換えます(フックがページのコンテキストで受け取るのも、これらの値です)。一方で、プラグインが追加した内容は、最初のものを採用する重複の除去によって、引き続き優先されます。linkの rel は、セキュリティのために固定された許可リスト(canonicalalternateauthorlicensenlwebsite.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 () => {},
}

分離されたサンドボックスランナーは、有効なサンドボックス型プラグインを読み込み順に呼び出します。フックはそれぞれ独立させます。あるサンドボックス型プラグインが、別のサンドボックス型プラグインより先に実行されることを前提にしないでください。

エラーの扱い

サンドボックス型のフックが失敗したときの動きは、フックが実行される時点によって異なります。

  • content:beforeSave で例外がスローされると、CONTENT_HOOK_ERROR で保存が失敗します。編集者に具体的な検証の理由を見せたい場合は、説明した SAVE_REJECTED のエンベロープを返します。
  • content:beforeDelete から false を返すと、ゴミ箱への移動が止まります。このフックが例外をスローした場合、EmDashはエラーをログに出力し、削除を続けます。
  • コンテンツのafter系のフックは、処理が成功したあとに実行されます。そのエラーはログに出力され、処理を巻き戻すことはできません。
  • ライフサイクル、メディア、メール、コメントのフックは、それぞれのもとになる処理の取り決めに従います。失敗時の動きに頼る前に、フックリファレンスで個々の戻り値を確認してください。

プロセス内のプラグインは、完全な設定の形で errorPolicy: "abort" または "continue" を使えます。この設定は、分離されたサンドボックス型プラグインでは、どの環境でも同じように働く回復の手段にはなりません。

タイムアウト

プロセス内のフックのパイプラインの既定値は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 いいえ
page:metadata ページの描画 追加する内容、または null いいえ
page:fragments ページの描画(ネイティブ型のみ) 追加する内容、または null いいえ

イベントの型とハンドラーのシグネチャーの全体は、フックリファレンスを参照します。