첫 번째 네이티브 플러그인

이 페이지

이 가이드는 네이티브 플러그인을 처음부터 구축하는 과정을 안내합니다. 네이티브 플러그인은 Astro 사이트와 동일한 프로세스에서 실행되며 React 관리 페이지, Portable Text 컴포넌트, 페이지 프래그먼트를 포함한 런타임에 완전히 접근할 수 있습니다.

네이티브 플러그인과 샌드박스 플러그인 중 아직 결정하지 못했다면 먼저 플러그인 형식 선택을 읽어보세요.

두 조각, 하나 또는 두 개의 파일

  1. 디스크립터 팩토리format: "native"를 포함한 PluginDescriptor를 반환합니다.
  2. 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;

테스트

  1. EmDash가 설치된 테스트 사이트를 만듭니다.
  2. 플러그인을 astro.config.mjs에 등록합니다.
  3. 개발 서버를 시작하고 콘텐츠를 생성, 수정, 삭제하여 훅을 트리거합니다.
  4. 콘솔에서 ctx.log 출력을 확인하고 API 라우트를 통해 스토리지를 검증합니다.

다음 단계