管理画面

このページ

EmDash の管理画面は、Astro サイトに埋め込まれた React のシングルページアプリケーションです。編集者と管理者向けに、コンテンツ管理の UI を一通り提供します。

アーキテクチャの概要

┌────────────────────────────────────────────────────────────────┐
│                    Astro シェル                                │
│  /_emdash/admin/[...path].astro                              │
│                                                                │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                    React SPA                             │  │
│  │                                                          │  │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐   │  │
│  │  │  TanStack   │  │  TanStack   │  │     Kumo        │   │  │
│  │  │   Router    │  │   Query     │  │  コンポーネント   │   │  │
│  │  └─────────────┘  └─────────────┘  └─────────────────┘   │  │
│  │                                                          │  │
│  │  ┌────────────────────────────────────────────────────┐  │  │
│  │  │           REST API クライアント                     │  │  │
│  │  │           /_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/*プラグインページ

マニフェスト駆動の UI

管理画面はコレクションやプラグインをハードコードしません。サーバーからマニフェストを取得します。

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": "Audit Log",
			"adminPages": [{ "path": "history", "label": "Audit History" }],
			"widgets": [{ "id": "recent-activity", "title": "Recent Activity" }]
		}
	],
	"taxonomies": [{ "name": "category", "label": "Categories", "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 エンドポイント

管理画面は REST API のみで通信します。

コンテンツ 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 生成

スキーマ API

メソッドエンドポイント目的
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フィールド削除

メディア API

メソッドエンドポイント目的
GET/api/mediaメディア一覧
POST/api/media/upload-url署名付きアップロード URL
POST/api/media/:id/confirmアップロード完了の確認
DELETE/api/media/:idメディア削除
GET/api/media/file/:keyメディアファイルの配信

その他の API

エンドポイント目的
/api/settingsサイト設定(GET/POST)
/api/menus/*ナビゲーションメニュー
/api/widget-areas/*ウィジェット管理
/api/taxonomies/*タクソノミー用語
/api/admin/plugins/*プラグイン状態

ページネーション

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

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

次のページを取得:

GET /api/content/posts?cursor=eyJpZCI6IjAxSjEyMzQ1NiJ9

プラグインの管理 UI

プラグインはページやダッシュボードのウィジェットで管理画面を拡張できます。統合処理が静的 import 付きの仮想モジュールを生成します。

// virtual:emdash/plugin-admins(生成)
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",
	},
];

表示 URL: /_emdash/admin/plugins/seo/settings

ダッシュボードウィジェット

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

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

認証

管理シェルルートは Astro のミドルウェアで認証を強制します。

// ミドルウェアの簡略化した例
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 自体はログインを扱いません。セッション Cookie を設定するのは Astro のページです。

ロールベースのアクセス

ロールごとに見える管理画面の範囲が異なります。

ロール表示されるセクション
Editorダッシュボード、割り当てコレクション、メディア
Admin+コンテンツタイプ、全コレクション、設定
Developer+CLI アクセス、生成された型

マニフェストのエンドポイントは、リクエストユーザーのロールに応じてコレクションと機能を絞り込みます。

コンテンツエディタ

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

// エディタ描画の簡略例
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)を使います。

ユーザー入力 → TipTap(ProseMirror JSON)→ 保存 → Portable Text(DB)
読み込み → Portable Text(DB)→ TipTap(ProseMirror JSON)→ 表示

変換は読み込み/保存の境界で 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 のボディサイズ制限を回避し、実際のアップロード進捗も得られます。

次のステップ