네이티브 플러그인은 EmDash가 Astro 사이트와 같은 프로세스로 임포트하는 npm 패키지입니다. 이 튜토리얼에서는 콘텐츠 저장을 로그로 기록하는 플러그인을 만들고, 사이트에 설치한 다음, astro.config.mjs에 등록합니다.
React 관리자 컴포넌트, Astro 렌더링 컴포넌트, 신뢰할 수 있는 페이지 조각처럼 플러그인에 인프로세스 기능이 필요하면 네이티브 형식을 사용하세요. 훅, 라우트, 스토리지, Block Kit으로 기능을 충분히 구현할 수 있다면 샌드박스 플러그인으로 시작하세요. 플러그인 형식 선택에서 각 형식을 비교합니다.
사전 준비
pnpm을 사용하고 개발 서버를 실행할 수 있는 EmDash 사이트에서 시작하세요.
명령에서는 사이트 디렉터리를 my-emdash-site라고 부르고, 그 옆에 plugin-activity를 만듭니다. my-emdash-site를 사이트의 디렉터리 이름으로 바꾸세요.
패키지 생성 및 등록
-
사이트 옆에 네이티브 패키지 디렉터리를 만듭니다.
mkdir -p ../plugin-activity/src cd ../plugin-activity -
패키지 메타데이터와 빌드 스크립트를 추가합니다.
{ "name": "@example/plugin-activity", "version": "0.1.0", "type": "module", "main": "./dist/index.mjs", "exports": { ".": { "types": "./dist/index.d.mts", "import": "./dist/index.mjs" } }, "files": ["dist"], "scripts": { "build": "tsdown src/index.ts --format esm --dts --clean", "dev": "tsdown src/index.ts --format esm --dts --watch", "typecheck": "tsc --noEmit" }, "peerDependencies": { "emdash": "*" }, "devDependencies": { "emdash": "*", "tsdown": "^0.20.0", "typescript": "^5.9.0" } }npm 패키지 이름은
@example/plugin-activity입니다. 아래 런타임 코드는 플러그인 API 라우트 URL에 맞도록 스코프가 없는 플러그인 IDplugin-activity를 사용합니다. -
TypeScript 구성을 추가합니다.
{ "compilerOptions": { "target": "ES2022", "module": "preserve", "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "declaration": true, "outDir": "./dist", "rootDir": "./src" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } -
패키지 의존성을 설치합니다.
pnpm install -
콘텐츠 저장 훅이 있는
src/index.ts를 만듭니다.import { definePlugin } from "emdash"; import type { PluginDescriptor } from "emdash"; export interface ActivityPluginOptions { logUpdates?: boolean; } export function activityPlugin( options: ActivityPluginOptions = {}, ): PluginDescriptor<ActivityPluginOptions> { return { id: "plugin-activity", version: "0.1.0", format: "native", entrypoint: "@example/plugin-activity", options, }; } export function createPlugin(options: ActivityPluginOptions = {}) { return definePlugin({ id: "plugin-activity", version: "0.1.0", capabilities: ["content:read"], hooks: { "content:afterSave": async (event, ctx) => { if (!event.isNew && options.logUpdates === false) return; ctx.log.info("Content saved", { collection: event.collection, contentId: event.content.id, isNew: event.isNew, }); }, }, }); } export default createPlugin;content:afterSave에는content:read기능이 필요합니다. 이 기능이 없으면 EmDash는 훅을 건너뜁니다. -
패키지를 빌드합니다.
pnpm build -
로컬 패키지를 사이트에 설치합니다.
cd ../my-emdash-site pnpm add ../plugin-activity -
디스크립터 팩토리를 EmDash 통합에 등록합니다.
import { defineConfig } from "astro/config"; import emdash from "emdash/astro"; import { activityPlugin } from "@example/plugin-activity"; export default defineConfig({ integrations: [ emdash({ plugins: [activityPlugin({ logUpdates: true })], }), ], });네이티브 디스크립터는
sandboxed가 아니라plugins에 넣어야 합니다.sandboxed배열에 네이티브 디스크립터를 넣으면 EmDash가 거부합니다. -
사이트를 시작하고 관리자 패널에서 항목을 저장합니다.
pnpm dev서버 로그에는 컬렉션, 콘텐츠 ID, 항목이 새로 생성되었는지 여부와 함께
Content saved가 기록됩니다.
디스크립터와 런타임의 경계
패키지 내보내기에는 두 가지 역할이 있습니다. EmDash는 각각을 서로 다른 단계에서 사용합니다.
- 디스크립터 팩토리인
activityPlugin()은 Astro가 구성을 평가하는 동안 실행됩니다. 직렬화할 수 있는 빌드 시점 메타데이터인id,version,format,entrypoint,options를 반환합니다. React와 Astro 진입점도 이 디스크립터에 속합니다. - 이름 있는 내보내기
createPlugin()은 EmDash가 초기화될 때 실행됩니다. EmDash는entrypoint에서 이를 임포트하고, 직렬화된options를 전달하며,definePlugin()이 해석한 플러그인을 기대합니다.
이름 있는 내보내기 createPlugin은 필수입니다. 기본 내보내기는 패키지 사용자에게 유용할 수 있지만, EmDash의 네이티브 로더는 createPlugin을 이름으로 임포트합니다.
디스크립터와 definePlugin()에서 id와 version을 동일하게 유지하세요. 플러그인 ID는 plugin-activity처럼 스코프가 없는 케밥 케이스로 사용하고, npm 스코프는 패키지 이름과 entrypoint에 두세요. 그러면 ID를 API 라우트 URL의 단일 플러그인 세그먼트로 사용할 수 있습니다.
플러그인 식별과 버전 관리에 허용되는 ID와 버전 형식이 나와 있습니다.
런타임 동작은 definePlugin()에 작성합니다.
capabilities와allowedHostsstoragehooks와routesadmin설정, 페이지, 위젯, Portable Text 선언
디스크립터에는 Astro가 빌드 시점에 임포트하거나 노출해야 하는 정적 항목이 들어갑니다. 어떤 관리자 필드에 디스크립터와 런타임의 선언이 서로 일치해야 하는지는 각 주제별 가이드에서 확인하세요.
네이티브 라우트 핸들러
네이티브 라우트 핸들러는 하나의 RouteContext를 받습니다. 이는 검증된 입력과 요청 데이터를 일반 PluginContext와 결합한 것입니다.
routes: {
status: {
permission: "plugins:read",
handler: async (ctx) => ({
pluginId: ctx.plugin.id,
callerId: ctx.user?.id ?? null,
}),
},
},
이에 해당하는 샌드박스 핸들러는 (routeCtx, ctx)를 두 개의 인수로 받습니다. 인증, 권한, 입력 스키마, 라우트 URL은 그 밖에는 공통 API 라우트 규약을 따릅니다.
네이티브 라우트가 request.body를 선언하면 definePluginRoute()로 감싸세요. 이 헬퍼는
본문 모드에서 ctx.input을 추론합니다. response: "raw"가 있는 네이티브 라우트는 pluginResponse()를 반환합니다.
두 헬퍼 모두 emdash에서 임포트하세요. 공통 API 라우트 가이드에 본문 모드, 제한, 응답
정책, 호환성 기본값이 나와 있습니다.
시크릿과 바인딩 읽기
네이티브 플러그인의 훅과 라우트 핸들러는 Astro 컨텍스트가 아니라 플러그인 컨텍스트를 받으므로 그 안에서는 Astro.locals를 사용할 수 없습니다. 시크릿과 플랫폼 바인딩은 런타임 환경에서 읽으세요.
배포 시크릿은 process.env에서 읽습니다. Node.js에서는 호스팅 플랫폼의 시크릿 관리자가 제공합니다. Cloudflare Workers에서는 wrangler secret put으로 설정한 시크릿이 Worker 시크릿에 설명된 대로 호환성 날짜가 2025-04-01 이후인 nodejs_compat 플래그를 통해 process.env에 전달됩니다.
사이트 관리자가 입력하는 자격 증명은 대신 생성된 설정 폼에서 secret 필드를 선언하세요. EmDash는 저장하기 전에 EMDASH_ENCRYPTION_KEY로 값을 암호화하므로, 관리자가 필드를 저장하기 전에 이 키를 설정하세요. 플러그인은 ctx.settings.get()으로 값을 읽습니다.
Cloudflare Workers에서 큐, R2 버킷, send_email 같은 바인딩은 cloudflare:workers의 env에 있는 객체입니다. Astro는 Node.js에서 astro.config.mjs를 로드하고, 이 파일은 디스크립터 팩토리를 위해 플러그인 패키지를 임포트합니다. Node.js는 cloudflare:workers를 로드할 수 없으므로 모듈 맨 위가 아니라 핸들러 안에서 임포트하세요. 다음 훅은 저장된 각 항목의 ID를 ACTIVITY_QUEUE로 바인딩된 큐에 보냅니다.
hooks: {
"content:afterSave": async (event) => {
const { env } = await import("cloudflare:workers");
await env.ACTIVITY_QUEUE.send({ contentId: event.content.id });
},
},
cloudflare:workers 모듈은 Workers 런타임에서만 해석되므로, 이를 임포트하는 플러그인은 Cloudflare 어댑터를 사용하는 사이트에서만 동작합니다.
이 임포트의 타입을 검사하려면 @cloudflare/workers-types를 패키지의 devDependencies와 tsconfig.json의 compilerOptions.types에 추가하세요. 그런 다음 플러그인이 읽는 각 바인딩을 Cloudflare.Env 인터페이스에 선언합니다. 다음 선언은 앞선 예시의 큐에 타입을 지정합니다.
declare namespace Cloudflare {
interface Env {
ACTIVITY_QUEUE: Queue;
}
}
다른 표면 추가
- React 관리자 페이지와 위젯에서는 설정, 사용자 정의 페이지, 대시보드 위젯, 필드 위젯, 편집기 패널, 목록 열을 다룹니다.
- Portable Text 렌더링 컴포넌트에서는 플러그인 블록용 Astro 컴포넌트를 등록합니다.
- 페이지 조각에서는 신뢰할 수 있는 스크립트나 HTML을 공개 페이지에 추가합니다.
- 네이티브 플러그인 배포에서는 빌드와 소스 진입점을 npm용으로 패키징합니다.