最初のネイティブプラグイン

このページ

このガイドでは、ネイティブプラグインをゼロから構築する手順を説明します。ネイティブプラグインは Astro サイトと同じプロセスで実行され、React 管理ページ、Portable Text コンポーネント、ページフラグメントを含むランタイムへの完全なアクセスが可能です。

ネイティブプラグインかサンドボックスプラグインかまだ決めていない場合は、先にプラグインフォーマットの選択をお読みください。

2つのパーツ、1つまたは2つのファイル

  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 ルートでストレージを検証します。

次のステップ