네이티브 플러그인은 커스텀 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" }],
},
});
EmDash가 빌드 시 컴포넌트를 찾을 수 있도록 디스크립터에 일치하는 adminEntry가 필요합니다.
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",
},
],
}
페이지 컴포넌트
다음 컴포넌트는 플러그인 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>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>
);
}
플러그인 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 }); // POST with JSON body
const result = await api.get("history?limit=50"); // query params supported
}
대시보드 위젯
위젯은 관리 대시보드에 표시되며 한눈에 정보를 제공합니다.
위젯 정의
id, 제목 및 크기를 사용하여 admin.widgets 아래에 각 위젯을 선언합니다.
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>
);
}
위젯 크기
| Size | Description |
|---|---|
full | 대시보드 전체 너비 |
half | 대시보드 절반 너비 |
third | 대시보드 1/3 너비 |
위젯은 화면 너비에 따라 자동으로 줄바꿈됩니다.
내보내기 구조
관리 진입점은 두 개의 객체를 내보냅니다.
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="Settings">
<Input label="API Key" type="password" />
<Toggle label="Enabled" defaultChecked />
<Button variant="primary">Save</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" }, // first
{ path: "/history", label: "History", icon: "history" }, // second
{ path: "/reports", label: "Reports", icon: "chart" }, // third
],
}
빌드 구성
관리 컴포넌트는 별도의 빌드 진입점이 필요합니다. 다음 번들러 구성은 서버 및 관리 진입점을 모두 빌드합니다.
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,
};