ネイティブプラグインは、カスタムReactページとダッシュボードウィジェットで管理パネルを拡張できます。サンドボックスプラグインは代わりにBlock KitとしてUIを記述します。プラグインのJavaScriptを管理画面に読み込むとサンドボックスの分離が壊れるためです。
プラグインに設定フォームだけが必要な場合、自動生成されるadmin.settingsSchemaフォーム(最初のネイティブプラグインを参照)がReactを書かずにほとんどのケースをカバーします。settingsSchemaが提供するよりもリッチなUIが必要な場合にのみカスタムコンポーネントを使用してください。
管理エントリーポイント
管理UIを持つプラグインは、adminエントリーポイントからpagesとwidgetsオブジェクトをエクスポートします:
import { SEOSettingsPage } from "./components/SEOSettingsPage";
import { SEODashboardWidget } from "./components/SEODashboardWidget";
export const widgets = {
"seo-overview": SEODashboardWidget,
};
export const pages = {
"/settings": SEOSettingsPage,
};
package.jsonでエントリーポイントを設定します:
{
"exports": {
".": "./dist/index.js",
"./admin": "./dist/admin.js"
}
}
definePlugin()から参照します:
definePlugin({
id: "seo",
version: "1.0.0",
admin: {
entry: "@my-org/plugin-seo/admin",
pages: [{ path: "/settings", label: "SEO Settings", icon: "settings" }],
widgets: [{ id: "seo-overview", title: "SEO Overview", size: "half" }],
},
});
ディスクリプタには対応するadminEntryが必要です。EmDashがビルド時にコンポーネントの場所を知るためです:
adminEntry: "@my-org/plugin-seo/admin",
管理ページ
管理ページは/_emdash/admin/plugins/<plugin-id>/<path>にマウントされるReactコンポーネントです。
ページ定義
admin.pagesの下に、パス、ラベル、アイコンを指定して各ページを宣言します:
admin: {
pages: [
{
path: "/settings",
label: "Settings",
icon: "settings",
},
{
path: "/reports",
label: "Reports",
icon: "chart",
},
],
}
ラベルは英語で宣言してください。管理画面はサイドバーとコマンドパレットをレンダリングする前に、共有のLinguiインスタンスを通してラベルを処理します。そのため、独自のメッセージカタログを読み込むプラグイン(英語のラベルをメッセージIDとして使用)は、ローカライズされたナビゲーションを無料で取得できます。管理画面自体のメッセージ(Settings、Dashboardなど)に一致するラベルは、プラグインカタログがなくても管理画面の翻訳を使用します。カタログにエントリがないラベルは宣言されたまま表示されます。
import { i18n } from "@lingui/core";
// プラグインのコンパイル済みカタログを管理画面のi18nインスタンスにマージします。
// 管理画面のロケールがドイツ語の場合、"Reports"は"Berichte"として表示されます。
const catalogs: Record<string, Record<string, string>> = {
de: { Reports: "Berichte", Settings: "Einstellungen" },
};
function mergeCatalog() {
const messages = catalogs[i18n.locale];
if (messages && !("Reports" in i18n.messages)) i18n.load(i18n.locale, messages);
}
mergeCatalog();
// 管理画面のロケール切り替えは変更時にカタログを*置き換える*ため、再マージが必要です。
// 上のセンチネルチェックが再帰を防ぎます(load()が"change"を発火するため)。
i18n.on("change", mergeCatalog);
ページコンポーネント
以下のコンポーネントはプラグインAPIフックを通じて設定を読み取り、保存します:
import { useState, useEffect } from "react";
import { usePluginAPI } from "@emdash-cms/admin";
export function SettingsPage() {
const api = usePluginAPI();
const [settings, setSettings] = useState<Record<string, unknown>>({});
const [saving, setSaving] = useState(false);
useEffect(() => {
api.get("settings").then(setSettings);
}, []);
const handleSave = async () => {
setSaving(true);
await api.post("settings/save", settings);
setSaving(false);
};
return (
<div>
<h1>プラグイン設定</h1>
<label>
サイトタイトル
<input
type="text"
value={(settings.siteTitle as string) || ""}
onChange={(e) => setSettings({ ...settings, siteTitle: e.target.value })}
/>
</label>
<button onClick={handleSave} disabled={saving}>
{saving ? "保存中..." : "設定を保存"}
</button>
</div>
);
}
プラグインAPIフック
usePluginAPI()はプラグインIDプレフィックスとX-EmDash-Request: 1 CSRFヘッダーを自動的に付加してプラグインのルートを呼び出します:
import { usePluginAPI } from "@emdash-cms/admin";
function MyComponent() {
const api = usePluginAPI();
const data = await api.get("status"); // GET /_emdash/api/plugins/<id>/status
await api.post("settings/save", { enabled: true }); // JSONボディでPOST
const result = await api.get("history?limit=50"); // クエリパラメータ対応
}
ダッシュボードウィジェット
ウィジェットは管理ダッシュボードに表示され、一目で情報を提供します。
ウィジェット定義
admin.widgetsの下に、ID、タイトル、サイズを指定して各ウィジェットを宣言します:
admin: {
widgets: [
{
id: "seo-overview",
title: "SEO Overview",
size: "half", // "full" | "half" | "third"
},
],
}
ウィジェットコンポーネント
以下のコンポーネントはマウント時にデータを取得し、コンパクトなサマリーをレンダリングします:
import { useState, useEffect } from "react";
import { usePluginAPI } from "@emdash-cms/admin";
export function SEOWidget() {
const api = usePluginAPI();
const [data, setData] = useState({ score: 0, issues: [] });
useEffect(() => {
api.get("analyze").then(setData);
}, []);
return (
<div className="widget-content">
<div className="score">{data.score}%</div>
<ul>
{data.issues.map((issue, i) => (
<li key={i}>{(issue as { message: string }).message}</li>
))}
</ul>
</div>
);
}
ウィジェットサイズ
| サイズ | 説明 |
|---|---|
full | ダッシュボード全幅 |
half | ダッシュボード半幅 |
third | ダッシュボード3分の1幅 |
ウィジェットは画面幅に応じて自動的に折り返されます。
エクスポート構造
管理エントリーポイントは2つのオブジェクトをエクスポートします:
import { SettingsPage } from "./components/SettingsPage";
import { ReportsPage } from "./components/ReportsPage";
import { StatusWidget } from "./components/StatusWidget";
import { OverviewWidget } from "./components/OverviewWidget";
export const pages = {
"/settings": SettingsPage,
"/reports": ReportsPage,
};
export const widgets = {
status: StatusWidget,
overview: OverviewWidget,
};
管理コンポーネントの使用
EmDashは一般的なパターン向けの組み込みコンポーネントを提供します:
import {
Card,
Button,
Input,
Select,
Toggle,
Table,
Pagination,
Alert,
Loading,
} from "@emdash-cms/admin";
function SettingsPage() {
return (
<Card title="設定">
<Input label="APIキー" type="password" />
<Toggle label="有効" defaultChecked />
<Button variant="primary">保存</Button>
</Card>
);
}
自動生成される設定UI
プラグインに設定フォームだけが必要な場合は、カスタムコンポーネントなしでadmin.settingsSchemaを使用します:
admin: {
settingsSchema: {
apiKey: { type: "secret", label: "API Key" },
enabled: { type: "boolean", label: "Enabled", default: true },
},
},
EmDashは自動的に設定ページを生成します。基本的なフォームを超える動作が必要な場合にのみカスタムReactページを使用してください。
ナビゲーション
プラグインページは管理サイドバーのプラグイン名の下に表示されます。順序は以下に示すようにadmin.pages配列に一致します:
admin: {
pages: [
{ path: "/settings", label: "Settings", icon: "settings" }, // 1番目
{ path: "/history", label: "History", icon: "history" }, // 2番目
{ path: "/reports", label: "Reports", icon: "chart" }, // 3番目
],
}
ビルド設定
管理コンポーネントには別のビルドエントリーポイントが必要です。以下のバンドラー設定はサーバーと管理の両方のエントリーポイントをビルドします:
tsdown
export default {
entry: {
index: "src/index.ts",
admin: "src/admin.tsx",
},
format: "esm",
dts: true,
external: ["react", "react-dom", "emdash", "@emdash-cms/admin"],
}; tsup
export default {
entry: ["src/index.ts", "src/admin.tsx"],
format: "esm",
dts: true,
external: ["react", "react-dom", "emdash", "@emdash-cms/admin"],
}; バンドルの重複を避けるため、ReactとEmDash adminを外部依存関係として保持してください。
プラグインの有効化/無効化
管理画面でプラグインが無効化された場合:
- サイドバーリンクが非表示になります。
- ダッシュボードウィジェットがレンダリングされません。
- 管理ページは404を返します。
- バックエンドフックは引き続き実行されます(データの安全性のため)。
プラグインは有効状態を確認できます:
const enabled = await ctx.kv.get<boolean>("_emdash:enabled");
完全な例
以下のプラグインは、ダッシュボードページ、設定ページ、ウィジェットを定義し、ランタイムと管理のエントリーポイントを別々のファイルに配置しています。src/index.tsファイルにはディスクリプタとランタイムが含まれています:
import { definePlugin } from "emdash";
import type { PluginDescriptor } from "emdash";
export function analyticsPlugin(): PluginDescriptor {
return {
id: "analytics",
version: "1.0.0",
format: "native",
entrypoint: "@my-org/plugin-analytics",
adminEntry: "@my-org/plugin-analytics/admin",
adminPages: [
{ path: "/dashboard", label: "Dashboard", icon: "chart" },
{ path: "/settings", label: "Settings", icon: "settings" },
],
adminWidgets: [{ id: "events-today", title: "Events Today", size: "third" }],
};
}
export function createPlugin() {
return definePlugin({
id: "analytics",
version: "1.0.0",
capabilities: ["network:request"],
allowedHosts: ["api.analytics.example.com"],
storage: {
events: { indexes: ["type", "createdAt"] },
},
admin: {
entry: "@my-org/plugin-analytics/admin",
settingsSchema: {
trackingId: { type: "string", label: "Tracking ID" },
enabled: { type: "boolean", label: "Enabled", default: true },
},
pages: [
{ path: "/dashboard", label: "Dashboard", icon: "chart" },
{ path: "/settings", label: "Settings", icon: "settings" },
],
widgets: [{ id: "events-today", title: "Events Today", size: "third" }],
},
routes: {
stats: {
handler: async (ctx) => {
const today = new Date().toISOString().split("T")[0];
const count = await ctx.storage.events.count({
createdAt: { gte: today },
});
return { today: count };
},
},
},
});
}
export default createPlugin;
src/admin.tsxファイルはページパスとウィジェットIDをReactコンポーネントにマッピングします:
import { EventsWidget } from "./components/EventsWidget";
import { DashboardPage } from "./components/DashboardPage";
import { SettingsPage } from "./components/SettingsPage";
export const widgets = {
"events-today": EventsWidget,
};
export const pages = {
"/dashboard": DashboardPage,
"/settings": SettingsPage,
};