サンドボックス化されたプラグインは、プラグインごとのKVストアに設定を保存し、編集UIをBlock Kitページとしてレンダリングします:フォームをJSONで記述し、ルートから提供します。
すべてはプラグインがフックとルートで既に使用している同じ仕組みを通じて行われます – 追加で学ぶことは何もありません。
KVストア
各プラグインは、任意のフックまたはルートでctx.kvとしてアクセス可能なプライベートなキーバリューストアを取得します。これは設定とその他の小さな永続状態の標準的な場所です:
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はプラグインごとです – 書き込むキーはプラグインIDの下に保存され、他のプラグインには見えません。
読み取りと書き込み
// 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 }, ...]
キーの命名規則
異なる種類の値を分離するためにプレフィックスを使用します。EmDashプラグイン全体の規則:
| プレフィックス | 目的 | 例 |
|---|---|---|
settings: | ユーザー設定可能な設定 | settings:apiKey |
state: | 内部プラグイン状態 | state:lastSync |
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);
Block Kitでの設定UI
サンドボックス化されたプラグインは、設定ページをBlock Kitとして記述します。管理者はプラグインのルート(慣例的にroutes.admin)にpage_loadインタラクションを送信し、プラグインはフォームのJSON記述を返します。ユーザーが保存をクリックすると、管理者はblock_actionまたはform_submitインタラクションを返します;プラグインはKVに書き込み、更新されたブロックを返します。
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);
}
}
}
設定ページを管理サイドバーに組み込むには、マニフェストで宣言します:
"admin": {
"pages": [{ "path": "/settings", "label": "Settings", "icon": "settings" }]
}
EmDashは、そのパスのpage_loadインタラクションを自動的にadminルートにルーティングします。
ブロックタイプ、フォームフィールド、条件付きフィールド、@emdash-cms/blocksビルダーヘルパーの完全なセットについては、Block Kitを参照してください。
秘密の値
Block Kitのsecret_inputフィールドはマスクされた入力としてレンダリングされます。ユーザーがそこに入力する値は慎重に扱ってください:
{
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: "",
}
保存時に、既存の秘密を毎回のセーブでクリアしないように空文字列をスキップします:
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
}
デフォルト値
KV読み取りは、書き込まれていないキーに対してnullを返します。読み取りサイトでデフォルト値を渡します:
const enabled = (await ctx.kv.get<boolean>("settings:enabled")) ?? true;
const maxItems = (await ctx.kv.get<number>("settings:maxItems")) ?? 100;
またはインストール時にデフォルト値を永続化します:
hooks: {
"plugin:install": async (_event, ctx) => {
await ctx.kv.set("settings:enabled", true);
await ctx.kv.set("settings:maxItems", 100);
},
},
トレードオフは、plugin:installがインストールごとに一度実行されることです。後のバージョンで新しい設定を出荷した場合、新規インストールのみがデフォルトを見ます – 既存のインストールには、plugin:activateでの移行(冪等:欠落している場合のみ書き込み)が必要か、読み取り時のフォールバックを使い続ける必要があります。
設定 vs. storage vs. KV
| ユースケース | メカニズム |
|---|---|
| 管理者編集可能な設定 | settings:プレフィックス付きのctx.kv + Block Kitページ |
| 内部プラグイン状態 | state:プレフィックス付きのctx.kv |
| ドキュメントコレクション(クエリ) | ctx.storage |
KVは文字列でキー付けされた小さな値用です – 設定、同期カーソル、キャッシュされた計算。クエリなし、インデックスなし。
Storageはインデックス付きクエリを持つドキュメントコレクション用です – フォーム送信、監査ログ、フィルタリング、ページング、カウントしたいもの。
ストレージレイアウト
KV値は、プラグイン名前空間付きキーを持つ_optionsテーブルに存在します。コードはsettings:apiKeyを使用します;EmDashはplugin:<your-plugin-id>:settings:apiKeyとして保存します。プレフィックスは自動的に追加され、1つのプラグインが別のプラグインのKVデータを読み取ったり上書きしたりするのを防ぎます。
ネイティブプラグイン:settingsSchema
ネイティブプラグインを書いている場合(React管理ページまたはPTコンポーネントが必要なため)、definePlugin()内で直接設定スキーマを宣言し、EmDashにフォームを自動生成させることができます。そのパスについては、ネイティブプラグインを参照してください。