Dieses Tutorial erstellt ein sandboxed Plugin, das Content-Save-Ereignisse aufzeichnet und eine kleine Health-Route bereitstellt. Sie scaffolden das Paket mit der Plugin-CLI, fügen einen Hook und eine Route hinzu, registrieren das Plugin bei einer EmDash-Site und bestätigen, dass beide Handler laufen.
Wenn Sie noch kein Plugin-Format gewählt haben, lesen Sie zuerst Plugin-Format wählen.
Voraussetzungen
Sie brauchen:
- Node.js und pnpm;
- eine EmDash-Site mit konfiguriertem Sandbox-Runner; und
- einen Atmosphere-Account-Handle oder DID für das Publisher-Feld des Manifests.
Das Plugin scaffolden
-
Führen Sie den Plugin-Scaffolder aus dem Verzeichnis aus, das das neue Projekt enthalten soll.
pnpm dlx @emdash-cms/plugin-cli init save-logDer Befehl fragt nach Publisher, Autor, Security-Kontakt und Quellrepository und zeigt dann eine Projektzusammenfassung, bevor er diese Struktur erstellt:
save-log/ ├── .agents/ │ └── skills -> ../skills ├── .claude/ │ ├── CLAUDE.md -> ../AGENTS.md │ └── skills -> ../skills ├── AGENTS.md ├── emdash-plugin.jsonc ├── package.json ├── pnpm-workspace.yaml ├── README.md ├── skills/ │ └── creating-plugins/SKILL.md ├── src/ │ └── plugin.ts ├── tests/ │ └── plugin.test.ts ├── tsconfig.json ├── vitest.config.ts └── .gitignore -
Installieren Sie die Abhängigkeiten des generierten Pakets.
cd save-log pnpm install
Zugriff und Storage des Plugins definieren
emdash-plugin.jsonc enthält Identität, Registry-Informationen und den Vertrauensvertrag des Plugins. Fügen Sie die Capability content:read hinzu, weil content:afterSave gespeicherten Inhalt dem Plugin exponiert. Deklarieren Sie eine Storage-Collection events, damit der Hook einen abfragbaren Datensatz jedes Speicherns behalten kann.
Das folgende Manifest enthält die in diesem Tutorial verwendeten Felder. Behalten Sie die vom Scaffolder erzeugten Publisher-, Autor- und Security-Werte.
{
"$schema": "./node_modules/@emdash-cms/plugin-cli/schemas/emdash-plugin.schema.json",
"slug": "save-log",
"publisher": "did:plc:abc123def456",
"license": "MIT",
"author": { "name": "Jane Doe", "url": "https://example.com" },
"security": { "email": "security@example.com" },
"description": "Records content-save events.",
"capabilities": ["content:read"],
"allowedHosts": [],
"storage": {
"events": { "indexes": ["savedAt"] },
},
}
Die Deklaration content:read teilt dem Site-Betreiber mit, dass der Hook gespeicherten Inhalt empfängt, und ist erforderlich, wenn dieses Plugin-Format in-process läuft. Der Zugriff auf ctx.storage.events würde werfen, wenn die Collection fehlte. Die Manifest-Referenz erklärt die übrigen Felder und Validierungsregeln.
Hook und Route hinzufügen
Ersetzen Sie die generierte src/plugin.ts durch die folgende Runtime-Definition:
import type { SandboxedPlugin } from "emdash/plugin";
const plugin: SandboxedPlugin = {
hooks: {
"content:afterSave": {
handler: async (event, ctx) => {
const savedAt = new Date().toISOString();
const contentId = String(event.content.id);
await ctx.storage.events.put(`${savedAt}:${contentId}`, {
savedAt,
collection: event.collection,
contentId,
});
ctx.log.info("Content save recorded", {
collection: event.collection,
contentId,
});
},
},
},
routes: {
health: {
public: true,
handler: async (_routeCtx, ctx) => {
return { ok: true, plugin: ctx.plugin.id };
},
},
},
};
export default plugin;
src/plugin.ts weist die Definition einer mit SandboxedPlugin typisierten Konstante zu und exportiert sie als Default. Die Annotation gibt Hook und Route ihre Parametertypen, ohne die EmDash-Runtime zum Bundle hinzuzufügen oder paketmanager-spezifische Deklarationspfade zu erzeugen.
Hook-Handler erhalten (event, ctx). Routen-Handler erhalten (routeCtx, ctx). Die Route health ist öffentlich und schreibgeschützt, sodass sie ohne Admin-Sitzung geprüft werden kann. Öffentliche Routen sind internetzugänglich; API-Routen erklärt Authentifizierung und Browser-Origin-Regeln, bevor Sie echte Daten oder Mutationen exponieren.
Den generierten Test aktualisieren
Der scaffoldete Test ruft die ursprüngliche Route hello über den Worker-Loader-Transport-Host auf. Ersetzen Sie ihn durch einen Test für die Route health:
import { afterEach, describe, expect, it } from "vitest";
import { createPluginTestHost, type PluginTestHost } from "@emdash-cms/plugin-test";
let host: PluginTestHost | undefined;
afterEach(async () => {
await host?.dispose();
host = undefined;
});
describe("health route", () => {
it("identifies the running plugin", async () => {
host = await createPluginTestHost();
const result = await host.invokeRoute("health");
expect(result).toEqual({ ok: true, plugin: "save-log" });
});
});
Der Test baut das Plugin und ruft die Route über Worker Loader und PluginBridge auf. Der sandboxed-Plugin-Testing-Guide deckt Hooks, Content-Fixtures, Storage-Assertions und die Grenzen lokaler workerd-Tests ab.
Validieren und bauen
Führen Sie den generierten Test aus, validieren Sie das Manifest und bauen Sie die npm-Artefakte.
pnpm run validate
pnpm run typecheck
pnpm run test
pnpm run build
Der Build erzeugt:
dist/plugin.mjsmit Hook- und Routencode;dist/manifest.jsonmit dem Runtime-Manifest und den gefundenen Hook- und Routennamen; unddist/index.mjs, den als Default exportierten Descriptor, den eine Site importiert.
dist/ ist generiertes Output. Der Scaffold schließt es von Git aus, weil der Plugin-Build es neu erzeugt.
Das Plugin registrieren
Installieren Sie das lokale Paket in Ihrer EmDash-Site. Führen Sie diesen Befehl aus dem Site-Verzeichnis aus und passen Sie den relativen Pfad an, wenn die Projekte keine Geschwister sind.
pnpm add file:../save-log
Importieren Sie den generierten Default-Export in astro.config.mjs und fügen Sie ihn zu sandboxed hinzu:
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import saveLog from "save-log";
export default defineConfig({
integrations: [
emdash({
sandboxed: [saveLog],
sandboxRunner: "@emdash-cms/sandbox-workerd/sandbox",
}),
],
});
Dieses Beispiel nutzt den Node.js-workerd-Runner. Behalten Sie den bereits von Ihrer Site konfigurierten Runner, wenn sie Cloudflare Workers oder ein anderes unterstütztes Setup nutzt.
Das Plugin ausführen
Starten Sie beide Entwicklungsprozesse:
- Führen Sie
pnpm devim Plugin-Verzeichnis aus. Die CLI baut das Plugin neu, wenn sich Quelle oder Manifest ändern. - Führen Sie den Entwicklungsbefehl der Site im Site-Verzeichnis aus.
Öffnen Sie die folgende Route auf der Site:
http://localhost:4321/_emdash/api/plugins/save-log/health
Die Antwort enthält den Standard-API-Envelope und den vom Plugin zurückgegebenen Wert:
{
"success": true,
"data": { "ok": true, "plugin": "save-log" },
}
Speichern Sie einen Eintrag im EmDash-Admin. Das Site-Log enthält Content save recorded, und der Hook schreibt ein Element in die events-Collection des Plugins.
Weiterbauen
- Hooks erklärt Hook-Ereignisse, Capabilities, Reihenfolge und Fehler.
- API-Routen deckt Validierung, Berechtigungen, öffentliche Routen und MCP-Exposition ab.
- Block Kit fügt eine Admin-Seite hinzu, ohne Browser-JavaScript auszuliefern.
- Einstellungen speichert site-spezifische Plugin-Konfiguration.
- Storage deckt indexierte Abfragen und Paginierung ab.
- Testing deckt direkte Sandbox-Transport-Tests und runtime-gestützte Host-Action-Tests ab.
- Bündeln und Veröffentlichen veröffentlicht das Plugin in der Registry.