API-Routen

Auf dieser Seite

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).

  • routeCtx enthält anfragebezogene Daten: { input, request, requestMeta }.
  • ctx ist derselbe PluginContext wie 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-IDRoutennameURL
formsstatus/_emdash/api/plugins/forms/status
formssubmissions/_emdash/api/plugins/forms/submissions
seosettings/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;
}