Les plugins sandboxed stockent leurs paramètres dans le magasin KV par plugin et rendent l’UI d’édition comme une page Block Kit : vous décrivez le formulaire en JSON et le servez depuis une route.
Tout se passe via la même machinerie que le plugin utilise déjà pour les hooks et les routes – il n’y a rien de supplémentaire à apprendre.
Le magasin KV
Chaque plugin obtient un magasin clé-valeur privé accessible via ctx.kv dans n’importe quel hook ou route. C’est l’endroit canonique pour les paramètres et tout autre petit état persistant :
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 est par plugin – les clés que vous écrivez sont stockées sous votre plugin id et ne sont pas visibles par les autres plugins.
Lecture et écriture
// 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 }, ...]
Conventions de nommage des clés
Utilisez des préfixes pour garder différents types de valeurs séparés. La convention dans les plugins EmDash :
| Préfixe | Objectif | Exemple |
|---|---|---|
settings: | Préférences configurables par l’utilisateur | settings:apiKey |
state: | État interne du plugin | state:lastSync |
cache: | Données en cache | 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);
UI des paramètres en Block Kit
Les plugins sandboxed décrivent leur page de paramètres comme Block Kit. L’admin envoie une interaction page_load à une route sur votre plugin (conventionnellement routes.admin), et le plugin renvoie une description JSON du formulaire. Quand l’utilisateur clique sur Sauvegarder, l’admin renvoie une interaction block_action ou form_submit ; le plugin écrit dans KV et renvoie des blocs mis à jour.
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);
}
}
}
Pour intégrer la page de paramètres dans la barre latérale d’administration, déclarez-la dans le manifeste :
"admin": {
"pages": [{ "path": "/settings", "label": "Settings", "icon": "settings" }]
}
EmDash route automatiquement les interactions page_load pour ce chemin vers votre route admin.
Voir Block Kit pour l’ensemble complet des types de blocs, champs de formulaire, champs conditionnels et les helpers de constructeur @emdash-cms/blocks.
Valeurs secrètes
Le champ secret_input de Block Kit est rendu comme une entrée masquée. Traitez toute valeur que l’utilisateur y entre avec précaution :
{
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: "",
}
Lors de la sauvegarde, ignorez les chaînes vides pour éviter d’effacer le secret existant à chaque sauvegarde :
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
}
Valeurs par défaut
Les lectures KV renvoient null pour les clés qui n’ont pas été écrites. Passez des valeurs par défaut au site de lecture :
const enabled = (await ctx.kv.get<boolean>("settings:enabled")) ?? true;
const maxItems = (await ctx.kv.get<number>("settings:maxItems")) ?? 100;
Ou persistez les valeurs par défaut pendant l’installation :
hooks: {
"plugin:install": async (_event, ctx) => {
await ctx.kv.set("settings:enabled", true);
await ctx.kv.set("settings:maxItems", 100);
},
},
Le compromis est que plugin:install s’exécute une fois par installation. Si vous livrez un nouveau paramètre dans une version ultérieure, seules les nouvelles installations voient la valeur par défaut – les installations existantes ont besoin soit d’une migration dans plugin:activate (idempotent : écrire uniquement si manquant) soit de continuer à utiliser le fallback au moment de la lecture.
Paramètres vs. storage vs. KV
| Cas d’usage | Mécanisme |
|---|---|
| Préférences éditables par l’admin | ctx.kv avec préfixe settings: + page Block Kit |
| État interne du plugin | ctx.kv avec préfixe state: |
| Collections de documents (requêtes) | ctx.storage |
KV est pour les petites valeurs identifiées par une chaîne – paramètres, curseurs de synchronisation, calculs en cache. Pas de requêtes, pas d’index.
Storage est pour les collections de documents avec des requêtes indexées – soumissions de formulaires, journaux d’audit, tout ce où vous voulez filtrer, paginer ou compter.
Disposition du stockage
Les valeurs KV se trouvent dans la table _options avec des clés namespacées par plugin. Votre code utilise settings:apiKey ; EmDash le stocke comme plugin:<your-plugin-id>:settings:apiKey. Le préfixe est ajouté automatiquement et empêche un plugin de lire ou d’écraser les données KV d’un autre.
Plugins natifs : settingsSchema
Si vous écrivez un plugin natif (parce que vous avez besoin de pages d’administration React ou de composants PT), vous pouvez déclarer un schéma de paramètres directement dans definePlugin() et laisser EmDash générer automatiquement le formulaire. Voir Plugins natifs pour cette voie.