Ce guide vous accompagne dans la création d’un plugin natif de zéro. Les plugins natifs s’exécutent dans le même processus que votre site Astro avec un accès complet au runtime, y compris les pages admin React, les composants Portable Text et les fragments de page.
Si vous n’avez pas encore décidé si vous voulez un plugin natif plutôt qu’un sandboxé, lisez d’abord Choisir un format de plugin.
Deux pièces, dans un ou deux fichiers
- Une fabrique de descripteur — retourne un
PluginDescriptoravecformat: "native". - Une fonction
createPlugin(options)— le côté runtime.
my-native-plugin/
├── src/
│ ├── index.ts # Fabrique de descripteur + createPlugin
│ ├── admin.tsx # Composants admin React (optionnel)
│ └── astro/ # Composants Astro pour le rendu de blocs PT (optionnel)
│ └── index.ts
├── package.json
└── tsconfig.json
Configurer le paquet
{
"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" }
}
Gardez emdash et react comme dépendances peer.
Écrire le descripteur et le runtime
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;
Règles d’identifiant de plugin
Le champ id doit correspondre à /^[a-z][a-z0-9_-]*$/.
Format de version
Utilisez le versionnage sémantique.
Enregistrer le plugin
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 des paramètres
admin: {
settingsSchema: {
apiKey: { type: "secret", label: "API Key" },
enabled: { type: "boolean", label: "Enabled", default: true },
maxItems: { type: "number", label: "Max items", min: 1, max: 1000, default: 100 },
},
},
Types de champs : string, number, boolean, select, secret, url, email. Les paramètres sont persistés dans le même magasin KV par plugin — lisez-les avec ctx.kv.get<T>("settings:<key>").
Exemple complet — plugin journal d’audit
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: "Rétention (jours)", description: "Jours de conservation. 0 = indéfiniment.", 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;
Tests
- Créez un site de test avec EmDash installé.
- Enregistrez votre plugin dans
astro.config.mjs. - Lancez le serveur de développement et déclenchez des hooks.
- Vérifiez la console et le stockage via les routes API.