コンテンツにスキップ

管理パネル

EmDash 管理パネルは、Astro サイトに埋め込まれた React シングルページアプリケーションです。編集者と管理者のための完全なコンテンツ管理インターフェースを提供します。

┌────────────────────────────────────────────────────────────────┐
│ Astro Shell │
│ /_emdash/admin/[...path].astro │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ React SPA │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │
│ │ │ TanStack │ │ TanStack │ │ Kumo │ │ │
│ │ │ Router │ │ Query │ │ Components │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ REST API Client │ │ │
│ │ │ /_emdash/api/* │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘

管理パネルは「大きな島」の React アプリです。Astro がシェルと認証を処理し、管理パネル内のすべてのナビゲーションとレンダリングはクライアントサイドで行われます。

レイヤー技術目的
ルーティングTanStack Router型安全なクライアントサイドルーティング
データTanStack Queryサーバー状態、キャッシュ、ミューテーション
UIKumoアクセシブルなコンポーネント (Base UI + Tailwind)
テーブルTanStack Tableソート、フィルタリング、ページネーション
フォームReact Hook Form + Zodサーバースキーマと一致するバリデーション
アイコンPhosphor一貫性のあるアイコン体系
エディターTipTapリッチテキスト編集 (Portable Text)

管理パネルは /_emdash/admin/ にマウントされ、クライアントサイドルーティングを使用します:

パス画面
/ダッシュボード
/content/:collectionコンテンツ一覧
/content/:collection/:idコンテンツエディター
/content/:collection/new新規エントリー
/mediaメディアライブラリ
/content-typesスキーマビルダー (管理者のみ)
/menusナビゲーションメニュー
/widgetsウィジェットエリア
/taxonomiesカテゴリー/タグ管理
/settingsサイト設定
/plugins/:pluginId/*プラグインページ

管理パネルは、コレクションやプラグインの知識をハードコードしません。代わりに、サーバーからマニフェストを取得します:

GET /_emdash/api/manifest

レスポンス:

{
"collections": [
{
"slug": "posts",
"label": "Blog Posts",
"labelSingular": "Post",
"icon": "file-text",
"supports": ["drafts", "revisions", "preview"],
"fields": [
{ "slug": "title", "type": "string", "required": true },
{ "slug": "content", "type": "portableText" }
]
}
],
"plugins": [
{
"id": "audit-log",
"label": "監査ログ",
"adminPages": [{ "path": "history", "label": "監査履歴" }],
"widgets": [{ "id": "recent-activity", "title": "最近のアクティビティ" }]
}
],
"taxonomies": [{ "name": "category", "label": "カテゴリー", "hierarchical": true }],
"version": "abc123"
}

管理パネルは、このマニフェストから完全にナビゲーション、フォーム、エディターを構築します。利点:

  • スキーマ変更が即時反映 — 管理パネルの再ビルドは不要
  • プラグイン UI が自動統合 — マニフェストからのページとウィジェット
  • 境界での型安全性 — Zod スキーマはサーバー側に保持
  1. 管理 SPA の読み込み — TanStack Router の初期化 2. マニフェストの取得 — TanStack Query によるコレクション/プラグインメタデータのキャッシュ 3. ナビゲーションの構築 — マニフェストからサイドバーを生成 4. ユーザーのナビゲート — クライアントサイドルーティング、ページ再読み込みなし 5. データの取得 — TanStack Query が REST API からコンテンツをリクエスト 6. フォームのレンダリング — マニフェストのフィールド記述子からフィールドエディターを生成 7. 変更の送信 — TanStack Query によるミューテーション、楽観的更新 8. サーバーでの検証 — サーバー側の Zod スキーマ、エラーは JSON として返却

管理パネルは REST API を通じてのみ通信します:

メソッドエンドポイント目的
GET/api/content/:collectionエントリー一覧
POST/api/content/:collectionエントリー作成
GET/api/content/:collection/:idエントリー取得
PUT/api/content/:collection/:idエントリー更新
DELETE/api/content/:collection/:idエントリーの論理削除
GET/api/content/:collection/:id/revisionsリビジョン一覧
POST/api/content/:collection/:id/preview-urlプレビュー URL 生成
メソッドエンドポイント目的
GET/api/schema完全なスキーマのエクスポート
GET/api/schema/collectionsコレクション一覧
POST/api/schema/collectionsコレクション作成
PUT/api/schema/collections/:slugコレクション更新
DELETE/api/schema/collections/:slugコレクション削除
POST/api/schema/collections/:slug/fieldsフィールド追加
PUT/api/schema/collections/:slug/fields/:fieldフィールド更新
DELETE/api/schema/collections/:slug/fields/:fieldフィールド削除
メソッドエンドポイント目的
GET/api/mediaメディアアイテム一覧
POST/api/media/upload-url署名付きアップロード URL 取得
POST/api/media/:id/confirmアップロード完了の確認
DELETE/api/media/:idメディアアイテム削除
GET/api/media/file/:keyメディアファイルの提供
エンドポイント目的
/api/settingsサイト設定 (GET/POST)
/api/menus/*ナビゲーションメニュー
/api/widget-areas/*ウィジェット管理
/api/taxonomies/*タクソノミーターム
/api/admin/plugins/*プラグイン状態

すべての一覧エンドポイントはカーソルベースのページネーションを使用します:

{
"items": [...],
"nextCursor": "eyJpZCI6IjAxSjEyMzQ1NiJ9"
}

次のページを取得:

GET /api/content/posts?cursor=eyJpZCI6IjAxSjEyMzQ1NiJ9

プラグインは、ページとダッシュボードウィジェットを使用して管理パネルを拡張できます。この統合により、静的インポートを含む仮想モジュールが生成されます:

// virtual:emdash/plugin-admins (generated)
import * as pluginAdmin0 from "@emdash-cms/plugin-seo/admin";
import * as pluginAdmin1 from "@emdash-cms/plugin-analytics/admin";
export const pluginAdmins = {
seo: pluginAdmin0,
analytics: pluginAdmin1,
};

プラグインページは /_emdash/admin/plugins/:pluginId/* の下にマウントされます:

// @emdash-cms/plugin-seo/src/admin.tsx
export const pages = [
{
path: "settings",
component: SEOSettingsPage,
label: "SEO Settings",
},
];

レンダリング先: /_emdash/admin/plugins/seo/settings

プラグインはダッシュボードにウィジェットを追加できます:

export const widgets = [
{
id: "seo-overview",
component: SEOWidget,
title: "SEO Overview",
size: "half", // "full" | "half" | "third"
},
];

管理シェルルートは、Astroミドルウェアを介して認証を強制します:

// Simplified middleware logic
export async function onRequest({ request, locals }, next) {
const session = await getSession(request);
if (request.url.includes("/_emdash/admin")) {
if (!session?.user) {
return redirect("/_emdash/admin/login");
}
locals.user = session.user;
}
return next();
}

管理SPA自体はログインを処理しません。それはセッションクッキーを設定するAstroページです。

異なるロールは管理パネルの異なる部分を閲覧できます:

ロール表示されるセクション
編集者ダッシュボード、割り当てられたコレクション、メディア
管理者+ コンテンツタイプ、すべてのコレクション、設定
開発者+ CLIアクセス、生成されたタイプ

マニフェストエンドポイントは、リクエストユーザーのロールに基づいてコレクションと機能をフィルタリングします。

コンテンツエディターは、フィールド定義に基づいて動的にフォームを生成します:

// Simplified editor rendering
function ContentEditor({ collection, fields }) {
return (
<form>
{fields.map((field) => (
<FieldWidget
key={field.slug}
type={field.type}
label={field.label}
required={field.required}
options={field.options}
/>
))}
</form>
);
}

各フィールドタイプには対応するウィジェットがあります:

フィールドタイプウィジェット
stringテキスト入力
textテキストエリア
number数値入力
booleanトグルスイッチ
datetime日付/時刻ピッカー
selectドロップダウン
multiSelectマルチセレクト
portableTextTipTapエディター
imageメディアピッカー
referenceエントリーピッカー

Portable Textフィールドは編集にTipTap(ProseMirror)を使用します:

User types → TipTap (ProseMirror JSON) → Save → Portable Text (DB)
Load → Portable Text (DB) → TipTap (ProseMirror JSON) → Display

変換は、portableTextToProsemirror() および prosemirrorToPortableText() を介して、読み込み/保存の境界で行われます。

サポートされているブロック:

  • 段落、見出し(H1-H6)
  • 箇条書きリストと番号付きリスト
  • ブロッククォート、コードブロック
  • 画像(メディアライブラリから)
  • リンク

プラグインやインポートからの不明なブロックは、読み取り専用のプレースホルダーとして保持されます。

メディアライブラリは以下を提供します:

  • グリッドビューとリストビュー
  • タイプ、日付による検索とフィルタリング
  • ドラッグアンドドロップアップロード
  • メタデータ付き画像プレビュー
  • 一括選択と削除

アップロードは、クライアントからストレージへの直接アップロードのために署名付きURLを使用します:

  1. アップロードURLをリクエスト — POST /api/media/upload-url 2. 直接アップロード — クライアントがファイルを 署名付きURL(R2/S3)にPUTします 3. アップロードを確認 — POST /api/media/:id/confirm 4. サーバーがメタデータを抽出 — 寸法、MIMEタイプなど

このアプローチは、Workersのボディサイズ制限を回避し、リアルタイムのアップロード進捗を提供します。