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äfix | Zweck | Beispiel |
|---|---|---|
settings: | Benutzerkonfigurierbare Werte | settings:apiKey |
state: | Persistenter interner Zustand | state:lastSync |
cache: | Wiederverwendbare berechnete oder remote Daten | cache: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
| Daten | Verwendung |
|---|---|
| Kleine benutzerkonfigurierbare Werte | ctx.kv mit settings:-Präfix |
| Kleiner interner Zustand oder Cursor | ctx.kv mit state:-Präfix |
| Abfragbare Datensätze wie Formulareinsendungen oder Logs | Eine deklarierte ctx.storage-Collection |
| Inhalte, die über den regulären EmDash-Editor bearbeitet werden | Eine 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.