훅은 플러그인이 이벤트에 응답하여 코드를 실행할 수 있게 해줍니다. 모든 훅은 이벤트 객체와 플러그인 컨텍스트를 받으며, 플러그인 정의 시점에 선언됩니다 — 런타임에서 동적 등록은 없습니다.
이 페이지는 샌드박스 플러그인을 다룹니다. 훅은 네이티브 플러그인에서도 동일하게 작동합니다. 네이티브 플러그인은 추가로 page:fragments를 등록할 수 있습니다.
훅 시그니처
모든 훅 핸들러는 두 개의 인수를 받습니다:
async (event, ctx) => ReturnType;
event— 방금 발생한 일에 대한 데이터 (콘텐츠 저장, 미디어 업로드, 라이프사이클 전환 등)ctx— 스토리지, KV, 로깅 및 기능 제어 API를 가진PluginContext
기본 내보내기의 satisfies SandboxedPlugin이 훅 이름(전체 정규 이벤트 타입)에서 event를 추론하고 ctx를 PluginContext로 추론하므로 핸들러에는 매개변수 어노테이션이 필요하지 않습니다. 헬퍼에서 이름으로 이벤트 타입을 참조하려면 emdash/plugin에서 가져오세요.
훅 구성
훅은 단순 핸들러로 또는 구성 객체로 감싸서 선언할 수 있습니다:
단순
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");
},
},
}, 구성 옵션
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
priority | number | 100 | 실행 순서. 낮은 숫자가 먼저 실행됩니다. |
timeout | number | 5000 | 최대 실행 시간(밀리초). |
dependencies | string[] | [] | 이 훅 전에 실행되어야 하는 플러그인 ID. |
errorPolicy | "abort" | "continue" | "abort" | 오류 시 파이프라인을 중지할지 여부. |
exclusive | boolean | false | 하나의 플러그인만 활성 제공자가 될 수 있습니다. email:deliver와 comment:moderate에 사용됩니다. |
handler | function | — | 훅 핸들러 함수. 필수. |
라이프사이클 훅
플러그인 설치, 활성화, 비활성화, 제거 시 실행됩니다.
plugin:install
플러그인이 처음 사이트에 추가될 때 한 번 실행됩니다.
"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) {
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를 반환합니다. 취소하려면 예외를 throw합니다.
"content:beforeSave": async (event, ctx) => {
const { content, collection } = event;
if (collection === "posts" && !content.title) {
throw new Error("Posts require a title");
}
if (typeof content.slug === "string") {
content.slug = content.slug.toLowerCase().replace(/\s+/g, "-");
}
return content;
},
이벤트: { content, collection, isNew } — 반환값: 수정된 콘텐츠 또는 void.
content:afterSave
콘텐츠가 성공적으로 저장된 후 실행됩니다. 알림, 로깅, 외부 동기화와 같은 부작용에 사용합니다.
"content:afterSave": async (event, ctx) => {
ctx.log.info(`${event.isNew ? "Created" : "Updated"} ${event.collection}/${event.content.id}`);
if (ctx.http) {
await ctx.http.fetch("https://api.example.com/webhook", {
method: "POST",
body: JSON.stringify({ event: "content:save", id: event.content.id }),
});
}
},
이벤트: { content, collection, isNew } — 반환값: Promise<void>
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 } — 반환값: boolean | void
content:afterDelete
콘텐츠가 성공적으로 삭제된 후 실행됩니다.
"content:afterDelete": async (event, ctx) => {
await ctx.storage.cache.delete(`${event.collection}:${event.id}`);
},
이벤트: { id, collection } — 반환값: Promise<void>
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>
미디어 훅
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}` };
},
이벤트: { file: { name, type, size } } — 반환값: 수정된 파일 또는 void
media:afterUpload
파일이 성공적으로 업로드된 후 실행됩니다.
이벤트: { media: { id, filename, mimeType, size, url, createdAt } } — 반환값: Promise<void>
퍼블릭 페이지 훅
이들은 플러그인이 렌더링된 퍼블릭 페이지에 기여할 수 있게 해줍니다. 템플릿은 emdash/ui의 <EmDashHead>, <EmDashBodyStart>, <EmDashBodyEnd> 컴포넌트를 포함하여 옵트인합니다.
page:metadata
<head>에 타입이 지정된 메타데이터를 기여합니다 — 메타 태그, OpenGraph 속성, 허용된 <link> rel, JSON-LD. 샌드박스 플러그인과 네이티브 플러그인 모두에서 사용 가능합니다. Core가 기여를 검증, 중복 제거, 렌더링합니다. 플러그인은 구조화된 데이터를 반환하며, 원시 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
기여 종류:
| 종류 | 렌더링 결과 | 중복 제거 키 |
|---|---|---|
meta | <meta name="..." content="..."> | key 또는 name |
property | <meta property="..." content="..."> | key 또는 property |
link | <link rel="canonical|alternate" href="..."> | canonical: 싱글톤; alternate: key 또는 hreflang |
jsonld | <script type="application/ld+json"> | id (있는 경우) |
각 중복 제거 키에 대해 첫 번째 기여가 우선됩니다. Link rel은 보안 잠금된 허용 목록(canonical, alternate, author, license, nlweb, site.standard.document)으로 제한됩니다. href는 HTTP 또는 HTTPS여야 합니다.
page:fragments
페이지 삽입 지점에 원시 HTML, 스크립트 또는 스타일시트를 기여합니다. 네이티브 플러그인만 가능.
샌드박스 플러그인은 이 훅을 사용할 수 없습니다. 출력이 방문자의 브라우저에서 퍼스트파티 코드로 실행되어 샌드박스 경계 밖에 있기 때문입니다. 샌드박스 안전한 페이지 기여에는 page:metadata를 사용하세요. 이 기능이 필요한 경우 네이티브 플러그인: 페이지 프래그먼트를 참조하세요.
훅 실행 순서
훅은 다음 순서로 실행됩니다:
- 낮은
priority값을 가진 훅이 먼저 실행됩니다. - 동일한 우선순위의 경우, 플러그인 등록 순서대로 실행됩니다.
dependencies가 있는 훅은 해당 플러그인이 완료될 때까지 기다립니다.
// 플러그인 A
"content:afterSave": { priority: 50, handler: async () => {} }
// 플러그인 B
"content:afterSave": { priority: 100, handler: async () => {} }
// 플러그인 C
"content:afterSave": {
priority: 200,
dependencies: ["plugin-a"], // 우선순위가 보통 더 나중이더라도 A를 기다림
handler: async () => {},
}
에러 처리
훅이 예외를 throw하거나 타임아웃될 때:
errorPolicy: "abort"— 전체 파이프라인이 중지되고 원래 작업이 실패할 수 있습니다.errorPolicy: "continue"— 오류가 기록되고 나머지 훅은 계속 실행됩니다.
"content:afterSave": {
timeout: 5000,
errorPolicy: "continue",
handler: async (event, ctx) => {
await ctx.http!.fetch("https://unreliable-api.com/notify");
},
},
타임아웃
훅의 기본 타임아웃은 5000ms입니다. 느린 작업에는 타임아웃을 늘리세요:
"content:afterSave": {
timeout: 30000,
handler: async (event, ctx) => {
// 장시간 실행되는 작업
},
},
훅 레퍼런스
| 훅 | 트리거 | 반환값 | 배타적 |
|---|---|---|---|
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 | 아니오 |
완전한 이벤트 타입과 핸들러 시그니처는 훅 레퍼런스를 참조하세요.