Sandboxed Plugins speichern ihre Einstellungen im pluginspezifischen KV-Store und rendern die Bearbeitungs-UI als Block Kit-Seite: Sie beschreiben das Formular in JSON und liefern es von einer Route aus.
Alles geschieht über dieselben Mechanismen, die das Plugin bereits für Hooks und Routes verwendet – es gibt nichts Zusätzliches zu lernen.
Der KV-Store
Jedes Plugin erhält einen privaten Key-Value-Store, der als ctx.kv in jedem Hook oder jeder Route zugänglich ist. Es ist der kanonische Ort für Einstellungen und jeden anderen kleinen persistenten Zustand:
interface KVAccess {
get<T>(key: string): Promise<T | null>;
set(key: string, value: unknown): Promise<void>;
delete(key: string): Promise<boolean>;
list(prefix?: string): Promise<Array<{ key: string; value: unknown }>>;
}
KV ist pluginspezifisch – Schlüssel, die Sie schreiben, werden unter Ihrer Plugin-ID gespeichert und sind für andere Plugins nicht sichtbar.
Lesen und Schreiben
// Read
const enabled = await ctx.kv.get<boolean>("settings:enabled");
const config = await ctx.kv.get<{ url: string; timeout: number }>("state:config");
// Write
await ctx.kv.set("settings:lastSync", new Date().toISOString());
await ctx.kv.set("state:cache", { data: items, expiry: Date.now() + 3600000 });
// Delete
const deleted = await ctx.kv.delete("state:tempData");
// List by prefix
const allSettings = await ctx.kv.list("settings:");
// → [{ key: "settings:enabled", value: true }, ...]
Namenskonventionen für Schlüssel
Verwenden Sie Präfixe, um verschiedene Arten von Werten getrennt zu halten. Die Konvention bei EmDash-Plugins:
| Präfix | Zweck | Beispiel |
|---|---|---|
settings: | Benutzerkonfigurierbare Einstellungen | settings:apiKey |
state: | Interner Plugin-Zustand | state:lastSync |
cache: | Gecachte Daten | cache:results |
// Clear prefixes
await ctx.kv.set("settings:webhookUrl", url);
await ctx.kv.set("state:lastRun", timestamp);
await ctx.kv.set("cache:feed", feedData);
// Avoid bare keys
await ctx.kv.set("url", url);
Einstellungs-UI in Block Kit
Sandboxed Plugins beschreiben ihre Einstellungsseite als Block Kit. Der Admin sendet eine page_load-Interaktion an eine Route in Ihrem Plugin (konventionell routes.admin), und das Plugin gibt eine JSON-Beschreibung des Formulars zurück. Wenn der Benutzer auf Speichern klickt, sendet der Admin eine block_action- oder form_submit-Interaktion zurück; das Plugin schreibt in KV und gibt aktualisierte Blöcke zurück.
import type { PluginContext, SandboxedPlugin } from "emdash/plugin";
interface BlockInteraction {
type: "page_load" | "block_action" | "form_submit";
page?: string;
action_id?: string;
values?: Record<string, unknown>;
}
export default {
routes: {
admin: {
handler: async (routeCtx, ctx) => {
const interaction = routeCtx.input as BlockInteraction;
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: [] };
},
},
},
} satisfies SandboxedPlugin;
async function renderSettings(ctx: PluginContext) {
const apiKey = (await ctx.kv.get<string>("settings:apiKey")) ?? "";
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",
initial_value: apiKey,
},
{
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: Record<string, unknown>) {
for (const [key, value] of Object.entries(values)) {
if (value !== undefined) {
await ctx.kv.set(`settings:${key}`, value);
}
}
}
Um die Einstellungsseite in die Admin-Seitenleiste zu integrieren, deklarieren Sie sie im Manifest:
"admin": {
"pages": [{ "path": "/settings", "label": "Settings", "icon": "settings" }]
}
EmDash leitet page_load-Interaktionen für diesen Pfad automatisch an Ihre admin-Route weiter.
Siehe Block Kit für die vollständige Palette von Blocktypen, Formularfeldern, bedingten Feldern und den @emdash-cms/blocks Builder-Helfern.
Geheime Werte
Das secret_input-Feld von Block Kit wird als maskierte Eingabe gerendert. Behandeln Sie jeden Wert, den der Benutzer dort eingibt, mit Vorsicht:
{
type: "secret_input",
action_id: "apiKey",
label: "API key",
// Don't seed initial_value with the real secret — pass an empty string or a sentinel,
// and only overwrite when the user enters a non-empty value.
initial_value: "",
}
Beim Speichern überspringen Sie leere Strings, um zu vermeiden, dass das vorhandene Geheimnis bei jedem Speichern gelöscht wird:
async function saveSettings(ctx: PluginContext, values: Record<string, unknown>) {
if (typeof values.apiKey === "string" && values.apiKey.length > 0) {
await ctx.kv.set("settings:apiKey", values.apiKey);
}
// ... other fields
}
Standardwerte
KV-Lesevorgänge geben null für Schlüssel zurück, die noch nicht geschrieben wurden. Übergeben Sie Standardwerte an der Lesestelle:
const enabled = (await ctx.kv.get<boolean>("settings:enabled")) ?? true;
const maxItems = (await ctx.kv.get<number>("settings:maxItems")) ?? 100;
Oder speichern Sie Standardwerte während der Installation:
hooks: {
"plugin:install": async (_event, ctx) => {
await ctx.kv.set("settings:enabled", true);
await ctx.kv.set("settings:maxItems", 100);
},
},
Der Kompromiss ist, dass plugin:install einmal pro Installation ausgeführt wird. Wenn Sie eine neue Einstellung in einer späteren Version ausliefern, sehen nur neue Installationen den Standard – bestehende Installationen benötigen entweder eine Migration in plugin:activate (idempotent: nur schreiben, wenn fehlend) oder müssen weiterhin den Fallback zur Lesezeit verwenden.
Einstellungen vs. Storage vs. KV
| Anwendungsfall | Mechanismus |
|---|---|
| Admin-bearbeitbare Einstellungen | ctx.kv mit settings:-Präfix + Block Kit-Seite |
| Interner Plugin-Zustand | ctx.kv mit state:-Präfix |
| Dokumentensammlungen (Abfragen) | ctx.storage |
KV ist für kleine Werte, die durch einen String gekennzeichnet sind – Einstellungen, Sync-Cursor, gecachte Berechnungen. Keine Abfragen, keine Indizes.
Storage ist für Dokumentensammlungen mit indizierten Abfragen – Formularübermittlungen, Audit-Logs, alles, wo Sie filtern, paginieren oder zählen möchten.
Speicher-Layout
KV-Werte befinden sich in der _options-Tabelle mit plugin-namespaceten Schlüsseln. Ihr Code verwendet settings:apiKey; EmDash speichert es als plugin:<your-plugin-id>:settings:apiKey. Das Präfix wird automatisch hinzugefügt und verhindert, dass ein Plugin die KV-Daten eines anderen liest oder überschreibt.
Native Plugins: settingsSchema
Wenn Sie ein natives Plugin schreiben (weil Sie React-Admin-Seiten oder PT-Komponenten benötigen), können Sie ein Einstellungsschema direkt in definePlugin() deklarieren und EmDash das Formular automatisch generieren lassen. Siehe Native Plugins für diesen Pfad.