이 가이드는 네이티브 플러그인을 처음부터 구축하는 과정을 안내합니다. 네이티브 플러그인은 Astro 사이트와 동일한 프로세스에서 실행되며 React 관리 페이지, Portable Text 컴포넌트, 페이지 프래그먼트를 포함한 런타임에 완전히 접근할 수 있습니다.
네이티브 플러그인과 샌드박스 플러그인 중 아직 결정하지 못했다면 먼저 플러그인 형식 선택을 읽어보세요.
두 조각, 하나 또는 두 개의 파일
- 디스크립터 팩토리 —
format: "native"를 포함한PluginDescriptor를 반환합니다. createPlugin(options)함수 — 런타임 측.
my-native-plugin/
├── src/
│ ├── index.ts # 디스크립터 팩토리 + createPlugin
│ ├── admin.tsx # React 관리 컴포넌트 (선택사항)
│ └── astro/
│ └── index.ts
├── package.json
└── tsconfig.json
패키지 설정
{
"name": "@my-org/plugin-analytics",
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"exports": {
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
"./admin": { "types": "./dist/admin.d.ts", "import": "./dist/admin.js" }
},
"files": ["dist"],
"peerDependencies": { "emdash": "*", "react": "^18.0.0" }
}
디스크립터와 런타임 작성
import { definePlugin } from "emdash";
import type { PluginDescriptor } from "emdash";
export interface AnalyticsOptions { enabled?: boolean; maxEvents?: number; }
export function analyticsPlugin(options: AnalyticsOptions = {}): PluginDescriptor {
return {
id: "analytics", version: "0.1.0", format: "native",
entrypoint: "@my-org/plugin-analytics", options,
adminEntry: "@my-org/plugin-analytics/admin",
adminPages: [{ path: "/dashboard", label: "Dashboard", icon: "chart" }],
adminWidgets: [{ id: "events-today", title: "Events Today", size: "third" }],
};
}
export function createPlugin(options: AnalyticsOptions = {}) {
const maxEvents = options.maxEvents ?? 100;
return definePlugin({
id: "analytics", version: "0.1.0",
capabilities: ["network:request"],
allowedHosts: ["api.analytics.example.com"],
storage: { events: { indexes: ["type", "createdAt"] } },
admin: {
entry: "@my-org/plugin-analytics/admin",
settingsSchema: {
trackingId: { type: "string", label: "Tracking ID" },
enabled: { type: "boolean", label: "Enabled", default: options.enabled ?? true },
},
pages: [{ path: "/dashboard", label: "Dashboard", icon: "chart" }],
widgets: [{ id: "events-today", title: "Events Today", size: "third" }],
},
hooks: {
"plugin:install": async (_event, ctx) => { ctx.log.info("Analytics plugin installed", { maxEvents }); },
"content:afterSave": async (event, ctx) => {
const enabled = await ctx.kv.get<boolean>("settings:enabled");
if (enabled === false) return;
await ctx.storage.events.put(`evt_${Date.now()}`, {
type: "content:save", contentId: event.content.id, createdAt: new Date().toISOString(),
});
},
},
routes: {
stats: {
handler: async (ctx) => {
const today = new Date().toISOString().split("T")[0];
const count = await ctx.storage.events.count({ createdAt: { gte: today } });
return { today: count };
},
},
},
});
}
export default createPlugin;
플러그인 ID 규칙
id 필드는 /^[a-z][a-z0-9_-]*$/와 일치해야 합니다.
버전 형식
시맨틱 버저닝을 사용하세요.
플러그인 등록
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { analyticsPlugin } from "@my-org/plugin-analytics";
export default defineConfig({
integrations: [
emdash({ plugins: [analyticsPlugin({ enabled: true, maxEvents: 500 })] }),
],
});
설정 UI
필드 타입: string, number, boolean, select, secret, url, email. 설정은 플러그인별 KV 저장소에 영속됩니다 — ctx.kv.get<T>("settings:<key>")로 읽습니다.
완전한 예제 — 감사 로그 플러그인
import { definePlugin } from "emdash";
import type { PluginDescriptor } from "emdash";
interface AuditEntry {
timestamp: string; action: "create" | "update" | "delete";
collection: string; resourceId: string;
}
export function auditLogPlugin(): PluginDescriptor {
return { id: "audit-log", version: "0.1.0", format: "native", entrypoint: "@emdash-cms/plugin-audit-log" };
}
export function createPlugin() {
return definePlugin({
id: "audit-log", version: "0.1.0",
storage: {
entries: { indexes: ["timestamp", "action", "collection", ["collection", "timestamp"], ["action", "timestamp"]] },
},
admin: {
settingsSchema: { retentionDays: { type: "number", label: "보존 기간(일)", description: "항목을 보존할 일수. 0 = 영구.", default: 90, min: 0, max: 365 } },
pages: [{ path: "/history", label: "Audit History", icon: "history" }],
widgets: [{ id: "recent-activity", title: "Recent Activity", size: "half" }],
},
hooks: {
"content:afterSave": {
priority: 200,
handler: async (event, ctx) => {
await ctx.storage.entries.put(`${Date.now()}-${event.content.id}`, {
timestamp: new Date().toISOString(), action: event.isNew ? "create" : "update",
collection: event.collection, resourceId: event.content.id as string,
});
},
},
"content:afterDelete": {
priority: 200,
handler: async (event, ctx) => {
await ctx.storage.entries.put(`${Date.now()}-${event.id}`, {
timestamp: new Date().toISOString(), action: "delete",
collection: event.collection, resourceId: event.id,
});
},
},
},
routes: {
recent: {
handler: async (ctx) => {
const result = await ctx.storage.entries.query({ orderBy: { timestamp: "desc" }, limit: 10 });
return { entries: result.items.map((item) => ({ id: item.id, ...(item.data as AuditEntry) })) };
},
},
},
});
}
export default createPlugin;
테스트
- EmDash가 설치된 테스트 사이트를 만듭니다.
- 플러그인을
astro.config.mjs에 등록합니다. - 개발 서버를 시작하고 콘텐츠를 생성, 수정, 삭제하여 훅을 트리거합니다.
- 콘솔에서
ctx.log출력을 확인하고 API 라우트를 통해 스토리지를 검증합니다.