Dieser Leitfaden führt Sie durch die Erstellung eines nativen Plugins von Grund auf. Native Plugins laufen im selben Prozess wie Ihre Astro-Site mit vollem Zugriff auf die Laufzeitumgebung, einschließlich React-Admin-Seiten, Portable Text-Komponenten und Seitenfragmenten.
Wenn Sie noch nicht entschieden haben, ob Sie ein natives Plugin anstelle eines sandboxed Plugins möchten, lesen Sie zuerst Auswahl eines Plugin-Formats. Native ist das Format für Plugins, die React-Admin-Seiten, Portable Text-Rendering-Komponenten oder Seitenfragmente benötigen.
Zwei Teile, in einer oder zwei Dateien
Wie sandboxed Plugins bestehen native Plugins aus zwei Teilen:
- Eine Descriptor Factory — gibt einen
PluginDescriptormitformat: "native"plus admin-bezogenen Einstiegspunkten zurück. Wird vonastro.config.mjszur Build-Zeit importiert. - Eine
createPlugin(options)-Funktion — die Laufzeitseite. Gibt eindefinePlugin({ id, version, capabilities, hooks, routes, admin })-Ergebnis zurück.
Im Gegensatz zu sandboxed Plugins können beide Teile in derselben Datei leben, da sie nicht in unterschiedlichen 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 components (optional)
│ └── astro/ # Astro components for PT block rendering (optional)
│ └── index.ts
├── package.json
└── tsconfig.json
Das Paket einrichten
Die folgende package.json deklariert die Einstiegspunkte und Peer-Dependencies, die ein natives Plugin benötigt:
{
"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-Dependencies, damit die Host-Site die tatsächlichen Versionen bereitstellt und Sie keine Duplikate ausliefern.
Den Descriptor und die Laufzeit schreiben
Die folgende src/index.ts definiert die Descriptor Factory und die createPlugin-Laufzeit in einer Datei:
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;
Wichtige Details dieser Konfiguration:
format: "native"ist erforderlich."native"ist auch der Standard, aber es explizit bei jedem Descriptor anzugeben, macht das Format leicht erkennbar.entrypointist der Haupt-Export des Pakets. EmDash importiert es zur Laufzeit und ruft den Standard-Export 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. Sandboxed Plugins haben diese Schnittstelle nicht — sie lesen Einstellungen stattdessen aus dem KV-Store.id,versionundcapabilitieserscheinen zweimal. Einmal im Descriptor, einmal indefinePlugin(). Sie sollten übereinstimmen. Die Kopie des Descriptors ist das, wasastro.config.mjszur Build-Zeit sieht; diedefinePlugin()-Kopie ist das, was zur Laufzeit ausgeführt wird.- 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 Standard-Formats. 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 — beginnen Sie mit einem Kleinbuchstaben, dann Buchstaben, Ziffern, Bindestriche oder Unterstriche. Die ID wird als einzelnes Pfadsegment in Plugin-Route-URLs und als Teil generierter SQL-Identifier für Plugin-Storage-Indizes verwendet, sodass alles außerhalb dieses Musters zur Laufzeit fehlschlägt. Die folgenden Werte zeigen, welche IDs akzeptiert werden:
// Valid
"seo";
"audit-log";
"audit_log";
"plugin-forms";
// Invalid
"@my-org/plugin-forms"; // scoped form not allowed at runtime
"MyPlugin"; // no uppercase
"42-plugin"; // can't start with a digit
"my.plugin"; // no dots
Paaren Sie eine unscoped id mit einem scoped npm-Paketnamen in entrypoint — der Paketname und die Plugin-ID sind separate Belange.
Versionsformat
Verwenden Sie semantische Versionierung. Die folgenden Werte zeigen, welche Versionsstrings akzeptiert werden:
version: "1.0.0"; // valid
version: "1.2.3-beta"; // valid (prerelease)
version: "1.0"; // invalid (missing patch)
Das Plugin registrieren
Importieren Sie in der astro.config.mjs Ihrer Site die Descriptor Factory und übergeben Sie sie in 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 Key" },
enabled: { type: "boolean", label: "Enabled", default: true },
maxItems: { type: "number", label: "Max items", 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 Pro-Plugin-KV-Store gespeichert, den auch sandboxed Plugins verwenden — lesen Sie sie mit ctx.kv.get<T>("settings:<key>") von überall.
Für umfangreichere Einstellungs-UIs als settingsSchema bietet, liefern Sie benutzerdefinierte React-Seiten — siehe React-Admin-Seiten und Widgets.
Vollständiges Beispiel — Audit-Log-Plugin
Das folgende Plugin zeichnet jede Inhaltserstellung, -aktualisierung und -löschung in indiziertem Speicher auf und stellt eine Route für kürzliche Aktivitäten bereit:
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: "Retention (days)",
description: "Days to keep entries. 0 = forever.",
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) => {
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 dem registrierten 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. - Starten Sie den Dev-Server 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.
Was kommt als Nächstes
- 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 plugin-definierte Blocktypen rendern.
- Seitenfragmente — injizieren Sie Skripte, Stylesheets oder HTML in öffentliche Seiten.
- Verteilung nativer Plugins — npm-Packaging und Versionierung.