Hooks

이 페이지

Hooks를 사용하면 플러그인이 이벤트에 대한 응답으로 코드를 실행할 수 있습니다. 모든 hooks는 이벤트 객체와 플러그인 컨텍스트를 받으며, 플러그인 정의 시점에 선언됩니다. 런타임에 동적 등록은 없습니다.

이 페이지는 샌드박스 플러그인을 다룹니다. Hooks는 네이티브 플러그인에서도 동일하게 작동합니다. 네이티브 플러그인은 추가로 page:fragments를 등록할 수 있습니다.

Hook 시그니처

모든 hook 핸들러는 두 개의 인수를 받습니다:

async (event, ctx) => ReturnType;
  • event — 방금 발생한 일에 대한 데이터 (콘텐츠 저장, 미디어 업로드, 라이프사이클 전환 등)
  • ctx — 스토리지, KV, 로깅 및 기능 제어 API가 있는 PluginContext

기본 내보내기의 satisfies SandboxedPlugin은 hook 이름(전체 정식 이벤트 타입)에서 event를 추론하고 ctxPluginContext로 추론하므로 핸들러에 매개변수 주석이 필요하지 않습니다. 헬퍼에서 이름으로 이벤트 타입을 참조하려면 emdash/plugin에서 가져오세요.

Hook 구성

Hook는 단순 핸들러로 선언하거나 구성 객체로 래핑할 수 있습니다:

간단함

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[][]이 hook보다 먼저 실행되어야 하는 플러그인 ID.
errorPolicy"abort" | "continue""abort"오류 발생 시 파이프라인을 중지할지 여부.
exclusivebooleanfalse하나의 플러그인만 활성 제공자가 될 수 있습니다. email:delivercomment:moderate에 사용됩니다.
handlerfunctionhook 핸들러 함수. 필수.

라이프사이클 hooks

플러그인 설치, 활성화, 비활성화 및 제거 중에 실행됩니다.

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>

콘텐츠 hooks

사이트 콘텐츠에 대한 생성, 업데이트 및 삭제 작업 중에 실행됩니다.

content:beforeSave

콘텐츠가 저장되기 전에 실행됩니다. 수정된 콘텐츠를 반환하거나 변경하지 않으려면 void를 반환합니다. 취소하려면 오류를 던지세요.

"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>

미디어 hooks

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>

공개 페이지 hooks

이를 통해 플러그인이 렌더링된 공개 페이지에 기여할 수 있습니다. 템플릿은 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 (있는 경우)

모든 중복 제거 키에 대해 첫 번째 기여가 우선합니다. 링크 rel은 보안 잠금 허용 목록(canonical, alternate, author, license, nlweb, site.standard.document)으로 제한됩니다. href는 HTTP 또는 HTTPS여야 합니다.

page:fragments

페이지 삽입 지점에 원시 HTML, 스크립트 또는 스타일시트를 기여합니다. 네이티브 플러그인만.

샌드박스 플러그인은 이 hook를 사용할 수 없습니다. 출력이 방문자의 브라우저에서 퍼스트 파티 코드로 실행되어 샌드박스 경계 밖에 있기 때문입니다. 샌드박스 안전 페이지 기여의 경우 page:metadata를 사용하세요. 이 표면이 필요한 경우 네이티브 플러그인: 페이지 프래그먼트를 참조하세요.

Hook 실행 순서

Hooks는 다음 순서로 실행됩니다:

  1. priority 값이 낮은 hooks가 먼저 실행됩니다.
  2. 우선순위가 같은 경우 hooks는 플러그인 등록 순서대로 실행됩니다.
  3. dependencies가 있는 hooks는 해당 플러그인이 완료될 때까지 기다립니다.
// 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 () => {},
}

오류 처리

hook가 오류를 던지거나 타임아웃되면:

  • errorPolicy: "abort" — 전체 파이프라인이 중지되고 원래 작업이 실패할 수 있습니다.
  • errorPolicy: "continue" — 오류가 로그에 기록되고 나머지 hooks는 계속 실행됩니다.
"content:afterSave": {
	timeout: 5000,
	errorPolicy: "continue",
	handler: async (event, ctx) => {
		await ctx.http!.fetch("https://unreliable-api.com/notify");
	},
},

타임아웃

Hooks의 기본값은 5000ms입니다. 더 느린 작업의 경우 타임아웃을 늘리세요:

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

Hook 참조

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

전체 이벤트 타입 및 핸들러 시그니처는 Hook 참조를 참조하세요.