設定
このページで分かること
- プラグインごとに分かれたKVの読み書きと、キーの接頭辞(
settings:、state:、cache:)の使い分け - Block Kitで設定ページを作り、入力値を検証して保存する方法
- KVは秘密情報を暗号化しないことと、既定値の扱い、KVとストレージの使い分け
このページの目次
サンドボックス型プラグインは、サイトごとの設定を、プラグイン専用のキーバリュー(KV)ストアに保存します。Block Kitの管理画面のページが、現在の値を読み込み、変更を受け付けて検証し、ctx.kv を通じて書き込みます。
KVの値を読み書きする
すべてのフックとルートは、ctx でこのKVのインターフェースを受け取ります。
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は、プラグインごとに名前空間が分かれています。2つのプラグインが同じキーを使っても、互いの値を読んだり上書きしたりすることはありません。
同じキーを複数のリクエストが同時に変更しうる場合は、条件付き書き込みを使って、古いリビジョンにもとづく更新を拒否します。同じメソッドが、ネイティブ型プラグインとサンドボックス型プラグインのどちらでも使えます。
ユーザーの設定を、内部の状態やキャッシュした値と分けるために、接頭辞を使います。
| 接頭辞 | 用途 | 例 |
|---|---|---|
settings: |
ユーザーが変更できる値 | settings:apiKey |
state: |
保持し続ける内部の状態 | state:lastSync |
cache: |
再利用できる計算結果や外部から取得したデータ | cache:feed |
次の呼び出しが、KVの操作をひととおり示しています。
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 は null を返します。list は、EmDashの内部のプラグインの名前空間の接頭辞を除いたキーを返します。
やさしい解説
公式の対応表では、WordPressのOptions APIにあたるのが、サイトの設定またはプラグインの ctx.kv です。ctx.kv はプラグインごとに分かれた保存場所で、ほかのプラグインと同じキーを使っても値は混ざりません。キーの先頭に settings:(ユーザーが変える設定)、state:(内部の状態)、cache:(キャッシュ)を付けて、用途ごとに分けます。
設定ページを追加する
プラグインの管理画面のナビゲーションに表示されるように、ページを emdash-plugin.jsonc で宣言します。
"admin": {
"pages": [{ "path": "/settings", "label": "Settings", "icon": "settings" }],
}
プラグインは、admin という名前の非公開のルートも用意する必要があります。EmDashは、ページが開かれたときに page_load を、ユーザーがフォームを送信したときに form_submit を送ります。
レスポンスの型を使い、やり取りを検証するために、@emdash-cms/blocks と zod を追加します。
pnpm add @emdash-cms/blocks zod
次のルートは、3つの値を読み込み、検証したフォームのフィールドだけを書き込みます。
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);
}
送信された値には、ユーザーが編集するまで秘密情報が含まれません。また、ユーザーがフィールドにフォーカスして中身を消した場合は、空の文字列が含まれることがあります。saveSettings は、送信された文字列が空でない場合にだけ、新しいAPIキーを書き込みます。ページは has_value を使い、値をブラウザに返さずに、保存済みの値があることを示します。
やり取り、ブロック、フォームの要素、ビルダー、条件付きのフィールドについては、Block Kitが正式なリファレンスです。
やさしい解説
サンドボックス型プラグインは、設定画面をBlock KitのJSONで返し、描画は管理画面が担当します。流れは3段階です。マニフェストで設定ページを宣言し、ページが開かれたとき(page_load)に現在の値を読んでフォームを返し、フォームが送信されたとき(form_submit)に値を検証して ctx.kv に保存します。
秘密情報の値
その保存方法が認証情報にとって許容できる場合にだけ、KVを使います。秘密情報をデプロイ環境のシークレットストアから取得する必要があり、EmDashのデータベースに書き込んではいけない場合は、ネイティブ型プラグインか、外部の認証情報サービスを使います。サンドボックス型プラグインは、ホストのプロセスの環境変数やプラットフォームのバインディングを読み取れません。
ユーザーが秘密情報を消す必要がある場合は、そのための独立した明示的な操作を用意します。伏せ字のフィールドが空であることを削除として扱うと、ユーザーが関係のない設定を保存したときに、動いている認証情報が消えてしまうことがあります。
既定値とアップグレード
キーを読み込むときに既定値を当てはめると、既存のインストールでも、マイグレーションなしで新しい設定を受け取れます。
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 の中で、足りないキーを何度実行しても同じ結果になる(冪等な)方法で初期化します。
KVとストレージの選び方
| データ | 使うもの |
|---|---|
| ユーザーが変更できる小さな値 | settings: 接頭辞を付けた ctx.kv |
| 小さな内部の状態やカーソル | state: 接頭辞を付けた ctx.kv |
| 送信データやログなど、検索したいレコード | 宣言した ctx.storage のコレクション |
| EmDashの通常のエディターで編集するコンテンツ | サイトのコンテンツのコレクション |
KVは、キーを指定した直接のアクセスと、接頭辞による一覧の取得に対応していますが、フィールドでの検索やインデックスはありません。インデックスを使った絞り込み、並べ替え、件数の取得、ページ分割ができるドキュメントコレクションは、ストレージが提供します。
ネイティブ型プラグインでは、代わりに definePlugin() の中で admin.settingsSchema を宣言し、フォームをEmDashに生成させることもできます。その形式については、はじめてのネイティブ型プラグインを参照します。