React admin pages and widgets
Native plugins can extend the admin panel with custom React pages and dashboard widgets — sandboxed plugins describe their UI as Block Kit instead, because shipping plugin JavaScript into the admin would break sandbox isolation.
If your plugin only needs a settings form, the auto-generated admin.settingsSchema form (see Your first native plugin) covers most cases without writing any React. Reach for custom components when you need richer UI than settingsSchema provides.
Admin entry point
Section titled “Admin entry point”Plugins with admin UI export pages and widgets objects from an admin entrypoint:
import { SEOSettingsPage } from "./components/SEOSettingsPage";import { SEODashboardWidget } from "./components/SEODashboardWidget";
export const widgets = { "seo-overview": SEODashboardWidget,};
export const pages = { "/settings": SEOSettingsPage,};Configure the entry point in package.json:
{ "exports": { ".": "./dist/index.js", "./admin": "./dist/admin.js" }}Reference it from 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" }], },});The descriptor needs a matching adminEntry so EmDash knows where to find the components at build time:
adminEntry: "@my-org/plugin-seo/admin",Admin pages
Section titled “Admin pages”Admin pages are React components that mount under /_emdash/admin/plugins/<plugin-id>/<path>.
Page definition
Section titled “Page definition”admin: { pages: [ { path: "/settings", label: "Settings", icon: "settings", }, { path: "/reports", label: "Reports", icon: "chart", }, ],}Page component
Section titled “Page component”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>Plugin Settings</h1>
<label> Site Title <input type="text" value={(settings.siteTitle as string) || ""} onChange={(e) => setSettings({ ...settings, siteTitle: e.target.value })} /> </label>
<button onClick={handleSave} disabled={saving}> {saving ? "Saving..." : "Save Settings"} </button> </div> );}Plugin API hook
Section titled “Plugin API hook”usePluginAPI() calls your plugin’s routes with the plugin id prefix and the X-EmDash-Request: 1 CSRF header added automatically:
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 }); // POST with JSON body const result = await api.get("history?limit=50"); // query params supported}Dashboard widgets
Section titled “Dashboard widgets”Widgets appear on the admin dashboard and provide at-a-glance information.
Widget definition
Section titled “Widget definition”admin: { widgets: [ { id: "seo-overview", title: "SEO Overview", size: "half", // "full" | "half" | "third" }, ],}Widget component
Section titled “Widget component”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> );}Widget sizes
Section titled “Widget sizes”| Size | Description |
|---|---|
full | Full dashboard width |
half | Half dashboard width |
third | One-third dashboard width |
Widgets wrap automatically based on screen width.
Export structure
Section titled “Export structure”The admin entry point exports two objects:
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,};Using admin components
Section titled “Using admin components”EmDash provides pre-built components for common patterns:
import { Card, Button, Input, Select, Toggle, Table, Pagination, Alert, Loading,} from "@emdash-cms/admin";
function SettingsPage() { return ( <Card title="Settings"> <Input label="API Key" type="password" /> <Toggle label="Enabled" defaultChecked /> <Button variant="primary">Save</Button> </Card> );}Auto-generated settings UI
Section titled “Auto-generated settings UI”If your plugin only needs a settings form, use admin.settingsSchema without custom components:
admin: { settingsSchema: { apiKey: { type: "secret", label: "API Key" }, enabled: { type: "boolean", label: "Enabled", default: true }, },},EmDash generates a settings page automatically. Reach for custom React pages only when you need behaviour beyond a basic form.
Navigation
Section titled “Navigation”Plugin pages appear in the admin sidebar under the plugin’s name. The order matches the admin.pages array.
admin: { pages: [ { path: "/settings", label: "Settings", icon: "settings" }, // first { path: "/history", label: "History", icon: "history" }, // second { path: "/reports", label: "Reports", icon: "chart" }, // third ],}Build configuration
Section titled “Build configuration”Admin components need a separate build entry point. Configure your bundler:
export default { entry: { index: "src/index.ts", admin: "src/admin.tsx", }, format: "esm", dts: true, external: ["react", "react-dom", "emdash", "@emdash-cms/admin"],};export default { entry: ["src/index.ts", "src/admin.tsx"], format: "esm", dts: true, external: ["react", "react-dom", "emdash", "@emdash-cms/admin"],};Keep React and EmDash admin as external dependencies to avoid bundling duplicates.
Plugin enable/disable
Section titled “Plugin enable/disable”When a plugin is disabled in the admin:
- Sidebar links are hidden.
- Dashboard widgets are not rendered.
- Admin pages return 404.
- Backend hooks still execute (for data safety).
Plugins can check their enabled state:
const enabled = await ctx.kv.get<boolean>("_emdash:enabled");Complete example
Section titled “Complete example”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;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,};