Dieser Leitfaden führt Sie durch den Aufbau eines nativen Plugins von Grund auf. Native Plugins laufen im gleichen Prozess wie Ihre Astro-Site — keine Sandbox-Grenze, voller Zugriff auf die Laufzeitumgebung und Zugriff auf Funktionen, die die Sandbox nicht bereitstellen kann (React-Admin-Seiten, Portable-Text-Komponenten, Seitenfragmente).
Falls Sie noch nicht entschieden haben, ob Sie ein natives Plugin statt eines Sandbox-Plugins möchten, lesen Sie zuerst Ein Plugin-Format wählen. Der native Pfad ist für Fälle, in denen die Sandbox wirklich nicht tun kann, was Sie benötigen.
Zwei Teile, in einer oder zwei Dateien
Wie Sandbox-Plugins bestehen native Plugins aus zwei Teilen:
- Eine Descriptor-Factory — gibt einen
PluginDescriptormitformat: "native"plus Admin-bezogene Einstiegspunkte zurück. Wird zur Buildzeit vonastro.config.mjsimportiert. - Eine
createPlugin(options)-Funktion — die Laufzeitseite. Gibt eindefinePlugin({ id, version, capabilities, hooks, routes, admin })-Ergebnis zurück.
Im Gegensatz zu Sandbox-Plugins können beide Teile in derselben Datei leben, da sie nicht in verschiedenen Umgebungen laufen — das gesamte Plugin läuft im Prozess. Der "."-Export des Pakets zeigt auf eine Datei, die sowohl die Descriptor-Factory als auch eine createPlugin- (oder default-) Funktion exportiert:
my-native-plugin/
├── src/
│ ├── index.ts # Descriptor-Factory + createPlugin
│ ├── admin.tsx # React-Admin-Komponenten (optional)
│ └── astro/ # Astro-Komponenten für PT-Block-Rendering (optional)
│ └── index.ts
├── package.json
└── tsconfig.json
Das Paket einrichten
{
"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"
}
}
Behalten Sie emdash und react als Peer-Abhängigkeiten bei, damit die Host-Site die tatsächlichen Versionen bereitstellt und Sie keine Duplikate ausliefern.
Den Descriptor und die Laufzeit schreiben
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: "Aktiviert", default: options.enabled ?? true },
},
pages: [{ path: "/dashboard", label: "Dashboard", icon: "chart" }],
widgets: [{ id: "events-today", title: "Ereignisse heute", size: "third" }],
},
hooks: {
"plugin:install": async (_event, ctx) => {
ctx.log.info("Analytics-Plugin installiert", { 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;
Einige Details, die Sie wissen sollten:
format: "native"ist erforderlich. Der Standardwert wäre auch"native"— aber explizit auf jedem Descriptor macht es einfach zu erkennen, mit welchem Format Sie arbeiten.entrypointist der Hauptexport des Pakets. EmDash importiert es zur Laufzeit und ruft den Standardexport auf, um das aufgelöste Plugin zu konstruieren.optionsfließen vom Descriptor zucreatePlugin. Alles, was der Benutzer beim Registrieren des Plugins übergibt (analyticsPlugin({ enabled: false })), wird im Descriptor gespeichert und ancreatePluginweitergeleitet. Sandbox-Plugins haben diese Oberfläche nicht — sie lesen Einstellungen stattdessen aus KV.id,versionundcapabilitieserscheinen zweimal. Einmal im Descriptor, einmal indefinePlugin(). Sie sollten übereinstimmen. Die Kopie des Descriptors ist das, wasastro.config.mjszur Buildzeit sieht; die Kopie vondefinePlugin()ist das, was zur Anforderungszeit läuft.- Native Route-Handler nehmen ein einzelnes Argument —
(ctx: RouteContext), wobeictx.input,ctx.requestundctx.requestMetamit den regulärenPluginContext-Eigenschaften zusammengeführt werden. Dies ist das Gegenteil der Zwei-Argument-Form des Standardformats. Siehe API-Routen für die vollständige Oberfläche (alles andere ist identisch).
Plugin-ID-Regeln
Das id-Feld muss /^[a-z][a-z0-9_-]*$/ entsprechen — mit einem Kleinbuchstaben beginnen, dann Buchstaben, Ziffern, Bindestriche oder Unterstriche. Die ID wird als einzelnes Pfadsegment in Plugin-Routen-URLs und als Teil von generierten SQL-Bezeichnern für Plugin-Speicher-Indizes verwendet, daher schlägt alles außerhalb dieses Musters zur Laufzeit fehl.
// Gültig
"seo";
"audit-log";
"audit_log";
"plugin-forms";
// Ungültig
"@my-org/plugin-forms"; // Scoped-Form zur Laufzeit nicht erlaubt
"MyPlugin"; // keine Großbuchstaben
"42-plugin"; // kann nicht mit einer Ziffer beginnen
"my.plugin"; // keine Punkte
Paaren Sie eine nicht-gescopte id mit einem gescopten npm-Paketnamen in entrypoint — der Paketname und die Plugin-ID sind separate Anliegen.
Versionsformat
Verwenden Sie semantische Versionierung:
version: "1.0.0"; // gültig
version: "1.2.3-beta"; // gültig (Vorabversion)
version: "1.0"; // ungültig (Patch fehlt)
Das Plugin registrieren
In der astro.config.mjs Ihrer Site importieren Sie die Descriptor-Factory und übergeben sie an das plugins: []-Array — native Plugins laufen immer im Prozess, niemals in sandboxed: []:
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 }),
],
}),
],
});
Einstellungs-UI
Native Plugins können admin.settingsSchema für ein automatisch generiertes Einstellungsformular verwenden, was der einfachste Weg ist:
admin: {
settingsSchema: {
apiKey: { type: "secret", label: "API-Schlüssel" },
enabled: { type: "boolean", label: "Aktiviert", default: true },
maxItems: { type: "number", label: "Max. Elemente", min: 1, max: 1000, default: 100 },
},
},
Feldtypen: string, number, boolean, select, secret, url, email. Jeder akzeptiert label, description, default plus typspezifische Extras wie min/max/options. Einstellungen werden im selben Per-Plugin-KV-Store gespeichert, den Sandbox-Plugins verwenden — lesen Sie sie mit ctx.kv.get<T>("settings:<key>") von überall.
Für eine reichhaltigere Einstellungs-UI als settingsSchema bietet, liefern Sie benutzerdefinierte React-Seiten — siehe React-Admin-Seiten und Widgets.
Vollständiges Beispiel: Audit-Log-Plugin
import { definePlugin } from "emdash";
import type { PluginDescriptor } from "emdash";
interface AuditEntry {
timestamp: string;
action: "create" | "update" | "delete";
collection: string;
resourceId: string;
userId?: 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: "Aufbewahrung (Tage)",
description: "Tage zum Aufbewahren von Einträgen. 0 = für immer.",
default: 90,
min: 0,
max: 365,
},
},
pages: [{ path: "/history", label: "Audit-Verlauf", icon: "history" }],
widgets: [{ id: "recent-activity", title: "Neueste Aktivität", size: "half" }],
},
hooks: {
"content:afterSave": {
priority: 200,
handler: async (event, ctx) => {
const entry: AuditEntry = {
timestamp: new Date().toISOString(),
action: event.isNew ? "create" : "update",
collection: event.collection,
resourceId: event.content.id as string,
};
await ctx.storage.entries.put(`${Date.now()}-${event.content.id}`, entry);
},
},
"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;
Testen
Testen Sie ein natives Plugin, indem Sie eine minimale Astro-Site mit registriertem Plugin erstellen:
- Erstellen Sie eine Test-Site mit installiertem EmDash.
- Registrieren Sie Ihr Plugin in
astro.config.mjsund importieren Sie es direkt aus Ihrem lokalen Quellpfad. - Führen Sie den Dev-Server aus und lösen Sie Hooks aus, indem Sie Inhalte erstellen, aktualisieren oder löschen.
- Überprüfen Sie die Konsole auf
ctx.log-Ausgaben und verifizieren Sie den Speicher über API-Routen.
Für Unit-Tests mocken Sie das PluginContext-Interface und rufen Hook-Handler direkt auf.
Nächste Schritte
- React-Admin-Seiten und Widgets — liefern Sie benutzerdefinierte React-UI für das Admin-Panel.
- Portable-Text-Rendering-Komponenten — stellen Sie Astro-Komponenten bereit, die vom Plugin definierte Blocktypen rendern.
- Seitenfragmente — injizieren Sie Skripte, Stylesheets oder HTML in öffentliche Seiten.
- Native Plugins verteilen — npm-Paketierung und Versionierung.