Einstellungen

Auf dieser Seite

Sandboxed Plugins speichern websitespezifische Einstellungen in ihrem privaten Key-Value-(KV-)Store. Eine Block Kit-Administrationsseite lädt die aktuellen Werte, nimmt Änderungen entgegen, validiert sie und schreibt sie über ctx.kv.

KV-Werte lesen und schreiben

Jeder Hook und jede Route erhält dieses KV-Interface über ctx:

interface KVAccess {
	get<T>(key: string): Promise<T | null>;
	getVersioned<T>(key: string): Promise<{ value: T; revision: string } | null>;
	compareAndSet(key: string, expectedRevision: string | null, value: unknown):
		Promise<{ applied: true; revision: string } | { applied: false }>;
	compareAndDelete(key: string, expectedRevision: string): Promise<{ applied: boolean }>;
	set(key: string, value: unknown): Promise<void>;
	delete(key: string): Promise<boolean>;
	list(prefix?: string): Promise<Array<{ key: string; value: unknown }>>;
}

KV ist nach Plugin getrennt. Zwei Plugins können denselben Schlüssel verwenden, ohne die Werte des jeweils anderen zu lesen oder zu überschreiben.

Wenn gleichzeitige Anfragen denselben Schlüssel ändern können, verwenden Sie bedingte Schreibvorgänge, um Aktualisierungen auf Basis einer veralteten Revision abzulehnen. Dieselben Methoden funktionieren sowohl in nativen als auch in Sandboxed Plugins.

Verwenden Sie Präfixe, um Benutzereinstellungen von internem Zustand und gecachten Werten zu trennen:

PräfixZweckBeispiel
settings:Benutzerkonfigurierbare Wertesettings:apiKey
state:Persistenter interner Zustandstate:lastSync
cache:Wiederverwendbare berechnete oder remote Datencache:feed

Die folgenden Aufrufe decken die KV-Operationen ab:

const enabled = await ctx.kv.get<boolean>("settings:enabled");
await ctx.kv.set("state:lastSync", new Date().toISOString());
const deleted = await ctx.kv.delete("cache:feed");
const allSettings = await ctx.kv.list("settings:");

get gibt null zurück, wenn der Schlüssel nicht existiert. list gibt Schlüssel ohne EmDashs internen Plugin-Namespace-Präfix zurück.

Eine Einstellungsseite hinzufügen

Deklarieren Sie die Seite in emdash-plugin.jsonc, damit sie in der Admin-Navigation des Plugins erscheint:

"admin": {
	"pages": [{ "path": "/settings", "label": "Settings", "icon": "settings" }],
}

Das Plugin muss außerdem eine private Route namens admin bereitstellen. EmDash sendet page_load, wenn die Seite geöffnet wird, und form_submit, wenn der Benutzer das Formular absendet.

Fügen Sie @emdash-cms/blocks und zod hinzu, um den Antworttyp zu verwenden und Interaktionen zu validieren:

pnpm add @emdash-cms/blocks zod

Die folgende Route lädt drei Werte und schreibt nur validierte Formularfelder:

import type { BlockResponse } from "@emdash-cms/blocks";
import type { PluginContext, SandboxedPlugin } from "emdash/plugin";
import { z } from "zod";

const interactionSchema = z.discriminatedUnion("type", [
	z.object({ type: z.literal("page_load"), page: z.string() }),
	z.object({
		type: z.literal("form_submit"),
		action_id: z.string(),
		block_id: z.string().optional(),
		values: z.object({
			apiKey: z.string().optional(),
			enabled: z.boolean(),
			maxItems: z.number().int().min(1).max(1000),
		}),
	}),
	z.object({
		type: z.literal("block_action"),
		action_id: z.string(),
		block_id: z.string().optional(),
		value: z.unknown().optional(),
	}),
]);

const plugin: SandboxedPlugin = {
	routes: {
		admin: {
			handler: async (routeCtx, ctx) => {
				const parsed = interactionSchema.safeParse(routeCtx.input);
				if (!parsed.success) return { blocks: [] };
				const interaction = parsed.data;

				if (interaction.type === "page_load" && interaction.page === "/settings") {
					return renderSettings(ctx);
				}

				if (interaction.type === "form_submit" && interaction.action_id === "save") {
					await saveSettings(ctx, interaction.values);
					return {
						...(await renderSettings(ctx)),
						toast: { message: "Settings saved", type: "success" },
					};
				}

				return { blocks: [] };
			},
		},
	},
};

export default plugin;

async function renderSettings(ctx: PluginContext): Promise<BlockResponse> {
	const apiKeyConfigured = (await ctx.kv.get<string>("settings:apiKey")) !== null;
	const enabled = (await ctx.kv.get<boolean>("settings:enabled")) ?? true;
	const maxItems = (await ctx.kv.get<number>("settings:maxItems")) ?? 100;

	return {
		blocks: [
			{ type: "header", text: "Plugin settings" },
			{
				type: "form",
				block_id: "settings",
				fields: [
					{
						type: "secret_input",
						action_id: "apiKey",
						label: "API key",
						has_value: apiKeyConfigured,
					},
					{
						type: "toggle",
						action_id: "enabled",
						label: "Enabled",
						initial_value: enabled,
					},
					{
						type: "number_input",
						action_id: "maxItems",
						label: "Max items",
						min: 1,
						max: 1000,
						initial_value: maxItems,
					},
				],
				submit: { label: "Save", action_id: "save" },
			},
		],
	};
}

async function saveSettings(
	ctx: PluginContext,
	values: { apiKey?: string; enabled: boolean; maxItems: number },
) {
	if (values.apiKey) await ctx.kv.set("settings:apiKey", values.apiKey);
	await ctx.kv.set("settings:enabled", values.enabled);
	await ctx.kv.set("settings:maxItems", values.maxItems);
}

Die übermittelten Werte lassen das Geheimnis weg, bis der Benutzer es bearbeitet, und können einen leeren String enthalten, wenn der Benutzer das Feld fokussiert und leert. saveSettings schreibt einen neuen API-Schlüssel nur, wenn der übermittelte String nicht leer ist. Die Seite verwendet has_value, um anzuzeigen, dass ein gespeicherter Wert existiert, ohne den Wert an den Browser zurückzugeben.

Block Kit ist die kanonische Referenz für Interaktionen, Blöcke, Formularelemente, Builder und bedingte Felder.

Geheime Werte

Verwenden Sie KV nur, wenn dieses Speichermodell für die Anmeldedaten akzeptabel ist. Wenn ein Geheimnis aus einem Deployment-Secret-Store kommen muss und nicht in die EmDash-Datenbank geschrieben werden darf, verwenden Sie ein natives Plugin oder einen externen Credential-Service. Sandboxed Plugins können weder die Umgebungsvariablen des Host-Prozesses noch die Plattform-Bindings lesen.

Bieten Sie eine separate, bewusste Aktion an, wenn Benutzer ein Geheimnis löschen müssen. Die Behandlung eines leeren maskierten Feldes als Löschung kann ein funktionierendes Credential löschen, wenn ein Benutzer eine nicht verwandte Einstellung speichert.

Standardwerte und Upgrades

Wenden Sie Standardwerte beim Lesen eines Schlüssels an, damit bestehende Installationen eine neue Einstellung ohne Migration erhalten:

const enabled = (await ctx.kv.get<boolean>("settings:enabled")) ?? true;
const maxItems = (await ctx.kv.get<number>("settings:maxItems")) ?? 100;

Sie können Anfangswerte während der Installation persistieren:

hooks: {
	"plugin:install": async (_event, ctx) => {
		await ctx.kv.set("settings:enabled", true);
		await ctx.kv.set("settings:maxItems", 100);
	},
},

plugin:install wird nur bei einer Neuinstallation ausgeführt. Wenn eine spätere Version eine Einstellung hinzufügt, wird es bei bestehenden Websites nicht erneut ausgeführt. Behalten Sie den Fallback zur Lesezeit bei, oder initialisieren Sie den fehlenden Schlüssel idempotent während plugin:activate.

KV oder Storage wählen

DatenVerwendung
Kleine benutzerkonfigurierbare Wertectx.kv mit settings:-Präfix
Kleiner interner Zustand oder Cursorctx.kv mit state:-Präfix
Abfragbare Datensätze wie Formulareinsendungen oder LogsEine deklarierte ctx.storage-Collection
Inhalte, die über den regulären EmDash-Editor bearbeitet werdenEine Website-Content-Collection

KV unterstützt direkten Schlüsselzugriff und Präfix-Auflistung, verfügt aber nicht über Feldabfragen oder Indizes. Storage bietet Dokumenten-Collections mit indizierter Filterung, Sortierung, Zählung und Paginierung.

Native Plugins können stattdessen admin.settingsSchema innerhalb von definePlugin() deklarieren und EmDash das Formular generieren lassen. Siehe Ihr erstes natives Plugin für dieses Format.