이 튜토리얼은 콘텐츠 저장 이벤트를 기록하고 작은 헬스 라우트를 노출하는 sandboxed 플러그인을 만듭니다. 플러그인 CLI로 패키지를 스캐폴드하고, 훅과 라우트를 하나씩 추가하고, EmDash 사이트에 등록한 뒤 두 핸들러가 모두 실행되는지 확인합니다.
아직 플러그인 형식을 고르지 않았다면 먼저 플러그인 형식 선택을 읽으세요.
사전 요구 사항
다음이 필요합니다.
- Node.js와 pnpm
- 샌드박스 러너가 구성된 EmDash 사이트
- 매니페스트의 publisher 필드용 Atmosphere 계정 핸들 또는 DID
플러그인 스캐폴드하기
-
새 프로젝트를 담을 디렉터리에서 플러그인 스캐폴더를 실행합니다.
pnpm dlx @emdash-cms/plugin-cli init save-log명령은 publisher, 작성자, 보안 연락처, 소스 저장소를 물은 뒤 이 구조를 만들기 전에 프로젝트 요약을 보여줍니다.
save-log/ ├── .agents/ │ └── skills -> ../skills ├── .claude/ │ ├── CLAUDE.md -> ../AGENTS.md │ └── skills -> ../skills ├── AGENTS.md ├── emdash-plugin.jsonc ├── package.json ├── pnpm-workspace.yaml ├── README.md ├── skills/ │ └── creating-plugins/SKILL.md ├── src/ │ └── plugin.ts ├── tests/ │ └── plugin.test.ts ├── tsconfig.json ├── vitest.config.ts └── .gitignore -
생성된 패키지의 의존성을 설치합니다.
cd save-log pnpm install
플러그인 접근과 스토리지 정의하기
emdash-plugin.jsonc에는 플러그인 신원, 레지스트리 정보, 신뢰 계약이 들어 있습니다. content:afterSave가 저장된 콘텐츠를 플러그인에 노출하므로 content:read 기능을 추가하세요. 훅이 각 저장의 조회 가능한 기록을 유지할 수 있도록 events 스토리지 컬렉션을 선언합니다.
다음 매니페스트에는 이 튜토리얼에서 쓰는 필드가 있습니다. 스캐폴더가 만든 publisher, author, security 값은 그대로 두세요.
{
"$schema": "./node_modules/@emdash-cms/plugin-cli/schemas/emdash-plugin.schema.json",
"slug": "save-log",
"publisher": "did:plc:abc123def456",
"license": "MIT",
"author": { "name": "Jane Doe", "url": "https://example.com" },
"security": { "email": "security@example.com" },
"description": "Records content-save events.",
"capabilities": ["content:read"],
"allowedHosts": [],
"storage": {
"events": { "indexes": ["savedAt"] },
},
}
content:read 선언은 훅이 저장된 콘텐츠를 받는다는 것을 사이트 운영자에게 알리며, 이 플러그인 형식이 인프로세스로 실행될 때 필요합니다. 컬렉션이 없으면 ctx.storage.events 접근이 예외를 던집니다. 매니페스트 참조가 나머지 필드와 검증 규칙을 설명합니다.
훅과 라우트 추가하기
생성된 src/plugin.ts를 다음 런타임 정의로 바꿉니다.
import type { SandboxedPlugin } from "emdash/plugin";
const plugin: SandboxedPlugin = {
hooks: {
"content:afterSave": {
handler: async (event, ctx) => {
const savedAt = new Date().toISOString();
const contentId = String(event.content.id);
await ctx.storage.events.put(`${savedAt}:${contentId}`, {
savedAt,
collection: event.collection,
contentId,
});
ctx.log.info("Content save recorded", {
collection: event.collection,
contentId,
});
},
},
},
routes: {
health: {
public: true,
handler: async (_routeCtx, ctx) => {
return { ok: true, plugin: ctx.plugin.id };
},
},
},
};
export default plugin;
src/plugin.ts는 정의를 SandboxedPlugin 타입 상수에 할당하고 기본으로 내보냅니다. 주석은 EmDash 런타임을 번들에 넣거나 패키지 매니저별 선언 경로를 만들지 않고 훅과 라우트에 매개변수 타입을 줍니다.
훅 핸들러는 (event, ctx)를 받습니다. 라우트 핸들러는 (routeCtx, ctx)를 받습니다. health 라우트는 공개이며 읽기 전용이라 관리 세션 없이 확인할 수 있습니다. 공개 라우트는 인터넷에 노출됩니다. 실제 데이터나 변경을 노출하기 전에 API 라우트가 인증과 브라우저 origin 규칙을 설명합니다.
생성된 테스트 업데이트하기
스캐폴드된 테스트는 Worker Loader 전송 호스트를 통해 원래 hello 라우트를 호출합니다. health 라우트 테스트로 바꾸세요.
import { afterEach, describe, expect, it } from "vitest";
import { createPluginTestHost, type PluginTestHost } from "@emdash-cms/plugin-test";
let host: PluginTestHost | undefined;
afterEach(async () => {
await host?.dispose();
host = undefined;
});
describe("health route", () => {
it("identifies the running plugin", async () => {
host = await createPluginTestHost();
const result = await host.invokeRoute("health");
expect(result).toEqual({ ok: true, plugin: "save-log" });
});
});
테스트는 플러그인을 빌드하고 Worker Loader와 PluginBridge를 통해 라우트를 호출합니다. sandboxed 플러그인 테스트 가이드는 훅, 콘텐츠 픽스처, 스토리지 어서션, 로컬 workerd 테스트의 한계를 다룹니다.
검증하고 빌드하기
생성된 테스트를 실행하고, 매니페스트를 검증하고, npm 산출물을 빌드합니다.
pnpm run validate
pnpm run typecheck
pnpm run test
pnpm run build
빌드는 다음을 만듭니다.
- 훅과 라우트 코드가 있는
dist/plugin.mjs - 런타임 매니페스트와 발견된 훅·라우트 이름이 있는
dist/manifest.json - 사이트가 가져오는 기본 내보내기 디스크립터
dist/index.mjs
dist/는 생성된 출력입니다. 플러그인 빌드가 다시 만들므로 스캐폴드는 Git에서 제외합니다.
플러그인 등록하기
로컬 패키지를 EmDash 사이트에 설치합니다. 사이트 디렉터리에서 이 명령을 실행하고, 프로젝트가 형제가 아니면 상대 경로를 조정하세요.
pnpm add file:../save-log
생성된 기본 내보내기를 astro.config.mjs에 가져오고 sandboxed에 추가합니다.
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import saveLog from "save-log";
export default defineConfig({
integrations: [
emdash({
sandboxed: [saveLog],
sandboxRunner: "@emdash-cms/sandbox-workerd/sandbox",
}),
],
});
이 예는 Node.js workerd 러너를 사용합니다. 사이트가 Cloudflare Workers나 다른 지원 설정을 쓰면 이미 구성된 러너를 유지하세요.
플러그인 실행하기
두 개발 프로세스를 모두 시작하세요.
- 플러그인 디렉터리에서
pnpm dev를 실행합니다. 소스나 매니페스트가 바뀌면 CLI가 플러그인을 다시 빌드합니다. - 사이트 디렉터리에서 사이트의 개발 명령을 실행합니다.
사이트에서 다음 라우트를 엽니다.
http://localhost:4321/_emdash/api/plugins/save-log/health
응답에는 표준 API 엔벨로프와 플러그인이 반환한 값이 들어 있습니다.
{
"success": true,
"data": { "ok": true, "plugin": "save-log" },
}
EmDash 관리에서 항목을 저장하세요. 사이트 로그에 Content save recorded가 있고, 훅은 플러그인의 events 컬렉션에 한 항목을 씁니다.