Plugins können API-Routen für ihre Admin-UI und externe Integrationen bereitstellen. Routen werden unter /_emdash/api/plugins/<slug>/<route-name> gemountet und laufen innerhalb der Sandbox-Laufzeit mit demselben PluginContext, den auch Hooks erhalten.
Diese Seite behandelt sandboxed Plugins. Die API-Oberfläche für native Plugins ist identisch; der einzige Unterschied ist die Handler-Signatur — siehe Native Plugins.
Routen definieren
import type { SandboxedPlugin } from "emdash/plugin";
import { z } from "astro/zod";
export default {
routes: {
status: {
handler: async (_routeCtx, ctx) => {
return { ok: true, plugin: ctx.plugin.id };
},
},
submissions: {
input: z.object({
formId: z.string().optional(),
limit: z.number().default(50),
cursor: z.string().optional(),
}),
handler: async (routeCtx, ctx) => {
const { formId, limit, cursor } = routeCtx.input;
const result = await ctx.storage.submissions.query({
where: formId ? { formId } : undefined,
orderBy: { createdAt: "desc" },
limit, cursor,
});
return result;
},
},
},
} satisfies SandboxedPlugin;
Sandboxed Routen-Handler nehmen zwei Argumente: (routeCtx, ctx).
routeCtxenthält anfragebezogene Daten:{ input, request, requestMeta }.ctxist derselbePluginContextwie in Hooks.
Indizierte Inhaltsfelder filtern
Plugins mit der content:read-Fähigkeit können benutzerdefinierte Felder filtern, die eine Sammlung als indexed markiert:
const result = await ctx.content.list("items", {
where: {
fieldFilters: {
priority: { in: ["urgent", "high"] },
score: { gte: 80 },
resolved: false,
},
},
});
Routen-URLs
| Plugin-ID | Routenname | URL |
|---|---|---|
forms | status | /_emdash/api/plugins/forms/status |
forms | submissions | /_emdash/api/plugins/forms/submissions |
seo | settings/save | /_emdash/api/plugins/seo/settings/save |
Authentifizierung und CSRF
Plugin-Routen sind standardmäßig authentifiziert. Setzen Sie permission auf eine engere EmDash-RBAC-Berechtigung:
routes: {
create: {
permission: "content:create",
input: z.object({ title: z.string() }),
handler: async (routeCtx, ctx) => { /* ... */ },
},
},
Für öffentliche Routen ohne Authentifizierung verwenden Sie public: true:
routes: {
track: {
public: true,
input: z.object({ event: z.string() }),
handler: async (routeCtx, ctx) => {
ctx.log.info("Tracked", { event: routeCtx.input.event });
return { ok: true };
},
},
},
Der authentifizierte Aufrufer
Bei privaten Routen ist routeCtx.user der authentifizierte Benutzer.
Öffentliche Antworten cachen
routes: {
catalog: {
public: true,
cacheControl: "public, max-age=60, stale-while-revalidate=300",
handler: async (ctx) => listProducts(ctx),
},
},
Route als MCP-Tool bereitstellen
mcp: {
tools: {
createEvent: {
description: "Create a calendar event when the user asks to add one.",
route: "events/create",
input: createEventInput,
output: z.object({ id: z.string() }),
destructive: false,
},
},
},
Eingabevalidierung
input akzeptiert ein Zod-Schema. Der Dispatcher parst den Anfrage-Body (POST/PUT/PATCH) oder den URL-Query-String (GET/HEAD/DELETE).
Query-String-Eingabe (GET/HEAD/DELETE)
routes: {
list: {
input: z.object({
status: z.enum(["open", "closed"]).optional(),
limit: z.coerce.number().int().max(100).default(20),
tag: z.array(z.string()).optional(),
}),
handler: async (routeCtx, ctx) => { /* ... */ },
},
},
Rückgabewerte
Geben Sie einen JSON-serialisierbaren Wert zurück. Der Dispatcher verpackt ihn in die Standard-Hülle ({ success: true, data: <Ihr Wert> }).
Fehler
Werfen Sie einen Fehler für eine Fehlerantwort. Für einen bestimmten Statuscode werfen Sie eine Response.
HTTP-Methoden
Routen antworten auf alle Methoden. Verzweigen Sie auf routeCtx.request.method.
Auf die Anfrage zugreifen
routeCtx.request ist ein SandboxedRequest: ein portabler { url, method, headers }-Datensatz.
Häufige Muster
Einstellungen über KV
routes: {
settings: {
handler: async (_routeCtx, ctx) => {
const settings = await ctx.kv.list("settings:");
const result: Record<string, unknown> = {};
for (const entry of settings) {
result[entry.key.replace("settings:", "")] = entry.value;
}
return result;
},
},
"settings/save": {
input: z.object({
enabled: z.boolean().optional(),
apiKey: z.string().optional(),
}),
handler: async (routeCtx, ctx) => {
for (const [key, value] of Object.entries(routeCtx.input)) {
if (value !== undefined) await ctx.kv.set(`settings:${key}`, value);
}
return { success: true };
},
},
},
Paginierte Liste
Externer API-Proxy
Leiten Sie eine Anfrage über ctx.http an einen externen Dienst weiter (erfordert network:request-Fähigkeit).
Routen aus der Admin-UI aufrufen
import { usePluginAPI } from "@emdash-cms/admin";
function SettingsPage() {
const api = usePluginAPI();
const handleSave = async (settings) => { await api.post("settings/save", settings); };
}
Routen aus Queue- und Scheduled-Handlern aufrufen
Verwenden Sie withEmDashRuntime() aus emdash/middleware.
Routen extern aufrufen
Öffentliche Routen sind direkt aufrufbar. Private Routen benötigen Sitzungs-Credentials oder einen API-Token.
Routenkontext-Referenz
interface SandboxedRouteContext {
input: unknown;
request: SandboxedRequest;
requestMeta?: unknown;
user?: UserInfo;
}
interface PluginContext {
plugin: { id: string; version: string };
storage: PluginStorage;
kv: KVAccess;
log: LogAccess;
site: SiteInfo;
url(path: string): string;
content?: ContentAccess;
taxonomies?: TaxonomyAccess;
media?: MediaAccess;
http?: HttpAccess;
users?: UserAccess;
email?: EmailAccess;
}