플러그인 훅

이 페이지

훅을 사용하면 플러그인이 이벤트에 반응해 코드를 실행할 수 있습니다. 모든 훅은 이벤트 객체와 플러그인 컨텍스트를 받습니다. 훅은 플러그인 정의 시점에 선언되며 런타임에 동적으로 등록되지 않습니다.

훅 시그니처

모든 훅 핸들러는 두 인자를 받습니다.

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활성 제공자는 하나의 플러그인만. email:delivercomment:moderate에 사용.
handlerfunction훅 핸들러 함수. 필수.

라이프사이클 훅

라이프사이클 훅은 플러그인 설치, 활성화, 비활성화 중에 실행됩니다.

plugin:install

플러그인이 사이트에 처음 추가될 때 한 번 실행됩니다.

"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아니오

전체 이벤트 타입과 핸들러 시그니처는 훅 참고를 보세요.