Este guia acompanha você na construção de um plugin nativo do zero. Plugins nativos executam no mesmo processo que seu site Astro com acesso total ao runtime, incluindo páginas admin React, componentes Portable Text e fragmentos de página.
Se você ainda não decidiu se quer um plugin nativo ao invés de um sandboxed, leia primeiro Escolhendo um formato de plugin.
Duas peças, em um ou dois arquivos
- Uma fábrica de descritor — retorna um
PluginDescriptorcomformat: "native". - Uma função
createPlugin(options)— o lado do runtime.
my-native-plugin/
├── src/
│ ├── index.ts # Fábrica de descritor + createPlugin
│ ├── admin.tsx # Componentes admin React (opcional)
│ └── astro/
│ └── index.ts
├── package.json
└── tsconfig.json
Configurar o pacote
{
"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" }
}
Escrever o descritor e o 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;
Regras de ID do plugin
O campo id deve corresponder a /^[a-z][a-z0-9_-]*$/.
Formato de versão
Use versionamento semântico.
Registrar o 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 de configurações
Tipos de campo: string, number, boolean, select, secret, url, email. Configurações são persistidas no mesmo armazenamento KV por plugin — leia-as com ctx.kv.get<T>("settings:<key>").
Exemplo completo — plugin de log de auditoria
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: "Retenção (dias)", description: "Dias para manter entradas. 0 = para sempre.", 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;
Testes
- Crie um site de teste com EmDash instalado.
- Registre seu plugin em
astro.config.mjs. - Execute o servidor de desenvolvimento e acione hooks.
- Verifique o console e o armazenamento via rotas API.