プラグインのフック

このページ

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

フックのシグネチャ

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

async (event: EventType, ctx: PluginContext) => ReturnType;
  • event — イベントに関するデータ(保存されるコンテンツ、アップロードされたメディアなど)
  • ctx — ストレージ、KV、ログ、capability で制御された API を含むプラグインコンテキスト

フックの設定

フックは単純なハンドラーとして、または完全な設定付きで宣言できます。

シンプル

hooks: {
  "content:afterSave": async (event, ctx) => {
    ctx.log.info("Content saved");
  }
}

完全な設定

hooks: {
  "content:afterSave": {
    priority: 100,
    timeout: 5000,
    dependencies: ["audit-log"],
    errorPolicy: "continue",
    handler: async (event, ctx) => {
      ctx.log.info("Content saved");
    }
  }
}

設定オプション

オプションデフォルト説明
prioritynumber100実行順。小さい値ほど先に実行されます。
timeoutnumber5000最大実行時間(ミリ秒)。
dependenciesstring[][]このフックより前に実行される必要があるプラグイン ID。
errorPolicy"abort" | "continue""abort"エラー時にパイプラインを止めるか。
exclusivebooleanfalseアクティブなプロバイダーは 1 つのプラグインのみ。email:delivercomment:moderate で使用。
handlerfunctionフックハンドラー関数。必須。

ライフサイクルフック

ライフサイクルフックは、プラグインのインストール、有効化、無効化の際に実行されます。

plugin:install

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

"plugin:install": async (_event, ctx) => {
  ctx.log.info("Installing plugin...");

  // Seed default data
  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");
  // Release resources, pause background work
}

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

plugin:uninstall

プラグインがサイトから削除されたときに実行されます。

"plugin:uninstall": async (event, ctx) => {
  ctx.log.info("Uninstalling plugin...");

  if (event.deleteData) {
    // User opted to delete plugin data
    const result = await ctx.storage.items!.query({ limit: 1000 });
    await ctx.storage.items!.deleteMany(result.items.map(i => i.id));
  }
}

イベント: { deleteData: boolean }
戻り値: Promise<void>

コンテンツフック

コンテンツフックは作成、更新、削除の操作中に実行されます。

content:beforeSave

コンテンツが保存される前に実行されます。変更後のコンテンツを返すか、void で変更なし。例外を投げると保存をキャンセルします。

"content:beforeSave": async (event, ctx) => {
  const { content, collection, isNew } = event;

  // Validate
  if (collection === "posts" && !content.title) {
    throw new Error("Posts require a title");
  }

  // Transform
  if (content.slug) {
    content.slug = content.slug.toLowerCase().replace(/\s+/g, "-");
  }

  return content;
}

イベント:

{
	content: Record<string, unknown>; // Content data being saved
	collection: string; // Collection name
	isNew: boolean; // True if creating, false if updating
}

戻り値: Promise<Record<string, unknown> | void>

content:afterSave

コンテンツの保存が成功した後に実行されます。通知、ログ、外部システムとの同期などの副作用に使います。

"content:afterSave": async (event, ctx) => {
  const { content, collection, isNew } = event;

  ctx.log.info(`${isNew ? "Created" : "Updated"} ${collection}/${content.id}`);

  // Trigger external sync
  if (ctx.http) {
    await ctx.http.fetch("https://api.example.com/webhook", {
      method: "POST",
      body: JSON.stringify({ event: "content:save", id: content.id })
    });
  }
}

イベント:

{
	content: Record<string, unknown>; // Saved content (includes id, timestamps)
	collection: string;
	isNew: boolean;
}

戻り値: Promise<void>

content:beforeDelete

コンテンツが削除される前に実行されます。削除をキャンセルするには false、許可するには true または void を返します。

"content:beforeDelete": async (event, ctx) => {
  const { id, collection } = event;

  // Prevent deletion of protected content
  if (collection === "pages" && id === "home") {
    ctx.log.warn("Cannot delete home page");
    return false;
  }

  return true;
}

イベント:

{
	id: string; // Content ID being deleted
	collection: string;
}

戻り値: Promise<boolean | void>

content:afterDelete

コンテンツの削除が成功した後に実行されます。

"content:afterDelete": async (event, ctx) => {
  const { id, collection } = event;

  ctx.log.info(`Deleted ${collection}/${id}`);

  // Clean up related plugin data
  await ctx.storage.cache!.delete(`${collection}:${id}`);
}

イベント:

{
	id: string;
	collection: string;
}

戻り値: Promise<void>

content:afterPublish

コンテンツが公開された後(ドラフトから本番へ昇格)に実行されます。キャッシュ無効化、通知、外部システムとの同期などの副作用に使います。 read:content capability が必要です。

"content:afterPublish": async (event, ctx) => {
  const { content, collection } = event;

  ctx.log.info(`Published ${collection}/${content.id}`);

  // Notify external system
  if (ctx.http) {
    await ctx.http.fetch("https://api.example.com/webhook", {
      method: "POST",
      body: JSON.stringify({ event: "content:publish", id: content.id })
    });
  }
}

イベント:

{
	content: Record<string, unknown>; // Published content (includes id, timestamps)
	collection: string;
}

戻り値: Promise<void>

content:afterUnpublish

コンテンツが非公開になった後(本番からドラフトへ戻す)に実行されます。キャッシュ無効化や外部システムへの通知などの副作用に使います。 read:content capability が必要です。

"content:afterUnpublish": async (event, ctx) => {
  const { content, collection } = event;

  ctx.log.info(`Unpublished ${collection}/${content.id}`);
}

イベント:

{
	content: Record<string, unknown>; // Unpublished content
	collection: string;
}

戻り値: Promise<void>

メディアフック

メディアフックはファイルアップロード中に実行されます。

media:beforeUpload

ファイルがアップロードされる前に実行されます。変更後のファイル情報を返すか、void でそのまま。例外を投げるとアップロードをキャンセルします。

"media:beforeUpload": async (event, ctx) => {
  const { file } = event;

  // Validate file type
  if (!file.type.startsWith("image/")) {
    throw new Error("Only images are allowed");
  }

  // Validate file size (10MB max)
  if (file.size > 10 * 1024 * 1024) {
    throw new Error("File too large");
  }

  // Rename file
  return {
    ...file,
    name: `${Date.now()}-${file.name}`
  };
}

イベント:

{
	file: {
		name: string; // Original filename
		type: string; // MIME type
		size: number; // Size in bytes
	}
}

戻り値: Promise<{ name: string; type: string; size: number } | void>

media:afterUpload

ファイルのアップロードが成功した後に実行されます。

"media:afterUpload": async (event, ctx) => {
  const { media } = event;

  ctx.log.info(`Uploaded ${media.filename}`, {
    id: media.id,
    size: media.size,
    mimeType: media.mimeType
  });
}

イベント:

{
	media: {
		id: string;
		filename: string;
		mimeType: string;
		size: number | null;
		url: string;
		createdAt: string;
	}
}

戻り値: Promise<void>

フックの実行順序

フックは次の順序で実行されます。

  1. priority の値が小さいフックが先に実行される
  2. 同じ優先度では、プラグイン登録順で実行される
  3. dependencies があるフックは、指定したプラグインの完了を待つ
// Plugin A
"content:afterSave": {
  priority: 50,  // Runs first
  handler: async () => {}
}

// Plugin B
"content:afterSave": {
  priority: 100,  // Runs second (default priority)
  handler: async () => {}
}

// Plugin C
"content:afterSave": {
  priority: 200,
  dependencies: ["plugin-a"],  // Runs after A, even if priority was lower
  handler: async () => {}
}

エラー処理

フックが例外を投げたりタイムアウトした場合:

  • errorPolicy: "abort" — パイプライン全体が停止します。元の操作が失敗する場合があります。
  • errorPolicy: "continue" — エラーはログに記録され、残りのフックは実行されます。
"content:afterSave": {
  timeout: 5000,
  errorPolicy: "continue",  // Don't fail the save if this hook fails
  handler: async (event, ctx) => {
    // External API call that might fail
    await ctx.http!.fetch("https://unreliable-api.com/notify");
  }
}

タイムアウト

フックのデフォルトタイムアウトは 5000ms(5 秒)です。時間がかかる操作には増やしてください。

"content:afterSave": {
  timeout: 30000,  // 30 seconds
  handler: async (event, ctx) => {
    // Long-running operation
  }
}

公開ページのフック

公開ページのフックにより、プラグインはレンダリングされたページの <head><body> に寄与できます。テンプレートは emdash/ui<EmDashHead><EmDashBodyStart><EmDashBodyEnd> コンポーネントでオプトインします。

page:metadata

<head> に型付きメタデータを寄与します — meta タグ、OpenGraph プロパティ、canonical/alternate リンク、JSON-LD 構造化データ。trusted モードと sandbox モードの両方で動作します。

コアが寄与を検証・重複排除・レンダリングします。プラグインは生の 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 };
  }
}

戻り値: PageMetadataContribution | PageMetadataContribution[] | null

寄与の種類:

Kindレンダリング重複排除キー
meta<meta name="..." content="...">key or name
property<meta property="..." content="...">key or property
link<link rel="canonical|alternate" href="...">canonical: singleton; alternate: key or hreflang
jsonld<script type="application/ld+json">id (if present)

各重複排除キーでは最初の寄与が採用されます。リンクの href は HTTP または HTTPS である必要があります。

page:fragments

ページの挿入位置に生の HTML、スクリプト、マークアップを寄与します。trusted プラグインのみ — sandbox 化されたプラグインはこのフックを使えません。

"page:fragments": async (event, ctx) => {
  return {
    kind: "external-script",
    placement: "head",
    src: "https://www.googletagmanager.com/gtm.js?id=GTM-XXXXX",
    async: true,
  };
}

戻り値: PageFragmentContribution | PageFragmentContribution[] | null

配置: "head""body:start""body:end"。配置用コンポーネントを省略したテンプレートは、その配置を対象とする寄与を黙って無視します。

フック一覧

Hookトリガー戻り値排他
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いいえ
media:beforeUploadファイルアップロード前変更後のファイル情報または voidいいえ
media:afterUploadファイルアップロード後voidいいえ
cronスケジュールタスク発火voidいいえ
email:beforeSendメール送信前変更後のメッセージ、false、または voidいいえ
email:delivertransport 経由でメール配信voidはい
email:afterSendメール送信後voidいいえ
comment:beforeCreateコメント保存前変更後のイベント、false、または voidいいえ
comment:moderateコメントステータスを決定{ status, reason? }はい
comment:afterCreateコメント保存後voidいいえ
comment:afterModerate管理者がコメントステータスを変更voidいいえ
page:metadataページレンダリング寄与または nullいいえ
page:fragmentsページレンダリング(trusted)寄与または nullいいえ

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