b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
4.2 KiB
4.2 KiB
Admin UI
Plugins extend the admin panel with React pages and dashboard widgets.
Entry Point
Export pages and widgets from src/admin.tsx:
// src/admin.tsx
import { SettingsPage } from "./components/SettingsPage";
import { ReportsPage } from "./components/ReportsPage";
import { StatusWidget } from "./components/StatusWidget";
// Pages keyed by path (trailing slash optional, so /settings and /settings/ both resolve)
export const pages = {
"/settings": SettingsPage,
"/reports": ReportsPage,
};
// Widgets keyed by ID (must match admin.widgets IDs)
export const widgets = {
status: StatusWidget,
};
Reference in plugin definition:
definePlugin({
id: "my-plugin",
version: "1.0.0",
admin: {
entry: "@my-org/my-plugin/admin",
pages: [
{ path: "/settings", label: "Settings", icon: "settings" },
{ path: "/reports", label: "Reports", icon: "chart" },
],
widgets: [{ id: "status", title: "Status", size: "half" }],
},
});
Pages mount at /_emdash/admin/plugins/<plugin-id>/<path>.
Pages
React components. Use usePluginAPI() to call plugin routes.
// src/components/SettingsPage.tsx
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>Settings</h1>
<label>
Site Title
<input
type="text"
value={settings.siteTitle || ""}
onChange={(e) => setSettings({ ...settings, siteTitle: e.target.value })}
/>
</label>
<button onClick={handleSave} disabled={saving}>
{saving ? "Saving..." : "Save"}
</button>
</div>
);
}
Widgets
Dashboard cards with at-a-glance info.
// src/components/StatusWidget.tsx
import { useState, useEffect } from "react";
import { usePluginAPI } from "@emdash-cms/admin";
export function StatusWidget() {
const api = usePluginAPI();
const [data, setData] = useState({ count: 0 });
useEffect(() => {
api.get("status").then(setData);
}, []);
return (
<div className="widget-content">
<div className="score">{data.count}</div>
</div>
);
}
Widget Sizes
| Size | Width |
|---|---|
full |
Full dashboard width |
half |
Half width |
third |
One-third width |
usePluginAPI()
Auto-prefixes plugin ID to route URLs:
const api = usePluginAPI();
const data = await api.get("status"); // GET /.../plugins/<id>/status
await api.post("settings/save", { enabled: true }); // POST with body
const result = await api.get("history?limit=50"); // Query params
Admin Components
Pre-built components from @emdash-cms/admin:
import { Card, Button, Input, Select, Toggle, Table, Loading, Alert } from "@emdash-cms/admin";
Auto-Generated Settings
If your plugin only needs settings, skip custom pages — use settingsSchema and EmDash generates the form:
admin: {
settingsSchema: {
apiKey: { type: "secret", label: "API Key" },
enabled: { type: "boolean", label: "Enabled", default: true },
}
}
Build Configuration
Admin components need a separate build entry:
// tsdown.config.ts
export default {
entry: {
index: "src/index.ts",
admin: "src/admin.tsx",
},
format: "esm",
dts: true,
external: ["react", "react-dom", "emdash", "@emdash-cms/admin"],
};
Keep React and @emdash-cms/admin as externals to avoid bundling duplicates.
Plugin Descriptor
The descriptor (returned by factory function) also declares admin metadata:
export function myPlugin(options = {}): PluginDescriptor {
return {
id: "my-plugin",
entrypoint: "@my-org/my-plugin",
version: "1.0.0",
options,
adminEntry: "@my-org/my-plugin/admin",
adminPages: [{ path: "/settings", label: "Settings", icon: "settings" }],
adminWidgets: [{ id: "status", title: "Status", size: "half" }],
};
}