このページはEmDashで作業する人向けであり、EmDashでサイトを構築する人向けではありません。内部メカニズム — テーブルレイアウト、Astro統合、リクエストパス、コード生成を文書化しています。EmDashを使用するためにこれらの知識は不要です。サイトを構築する場合は、代わりにアーキテクチャとコンテンツモデルをお読みください。
Astro統合
EmDashはemdashパッケージからAstro統合として実行されます。ビルド時に:
-
AstroのAPI
injectRouteでAdmin SPAとREST APIルートを注入します。ユーザーのプロジェクトには何もコピーされません。注入されるパスは:パスパターン 目的 /_emdash/admin/[...path]管理パネルSPA /_emdash/api/manifest管理マニフェスト(コレクション、プラグイン) /_emdash/api/content/[collection]コンテンツエントリCRUD /_emdash/api/media/*メディアライブラリ操作 /_emdash/api/schema/*スキーマ管理 /_emdash/api/settingsサイト設定 /_emdash/api/menus/*ナビゲーションメニュー /_emdash/api/taxonomies/*カテゴリー、タグ、カスタムタクソノミー -
バンドラーが設定コードとプラグインコードを解決してツリーシェイクできるように仮想モジュールを生成します:
モジュール 目的 virtual:emdash/configデータベースとストレージの設定 virtual:emdash/dialectデータベースダイアレクトファクトリー virtual:emdash/plugin-adminsプラグイン管理UIの静的インポート -
Live Collectionsローダーを提供し、マイグレーションを管理し、ストレージ接続を開きます。
データベースファーストスキーマ
スキーマ定義はコードではなくデータベースに存在します。2つのシステムテーブルが構造を追跡します。
_emdash_collectionsはコレクションごとに1行を保持します:
CREATE TABLE _emdash_collections (
id TEXT PRIMARY KEY,
slug TEXT UNIQUE NOT NULL, -- "posts", "products"
label TEXT NOT NULL, -- "Blog Posts"
label_singular TEXT, -- "Post"
description TEXT,
icon TEXT,
supports JSON, -- ["drafts", "revisions", "preview"]
source TEXT, -- 作成方法
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT
);
sourceカラムは出所を記録します:manual(管理UI)、template:<name>(シードファイル)、import:wordpress(インポーター)、またはdiscovered(既存テーブルから自動検出)。
_emdash_fieldsはフィールドごとに1行を保持し、コレクションにリンクされています:
CREATE TABLE _emdash_fields (
id TEXT PRIMARY KEY,
collection_id TEXT REFERENCES _emdash_collections(id),
slug TEXT NOT NULL, -- カラム名
label TEXT NOT NULL,
type TEXT NOT NULL, -- フィールドタイプ
column_type TEXT NOT NULL, -- TEXT, REAL, INTEGER, JSON
required INTEGER DEFAULT 0,
unique_field INTEGER DEFAULT 0,
default_value TEXT,
validation JSON,
widget TEXT,
options JSON,
sort_order INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(collection_id, slug)
);
コレクションごとのコンテンツテーブル
各コレクションはec_プレフィックスで独自のテーブルを取得します。titleとpriceフィールドを持つproductsコレクションは以下を生成します:
CREATE TABLE ec_products (
-- システムカラム、常に存在
id TEXT PRIMARY KEY,
slug TEXT UNIQUE,
status TEXT DEFAULT 'draft',
author_id TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
published_at TEXT,
deleted_at TEXT, -- ソフトデリート
version INTEGER DEFAULT 1, -- 楽観的ロック
-- コンテンツカラム、フィールド定義から
title TEXT NOT NULL,
price REAL
);
実カラム(JSONブロブを持つ1つのテーブルではなく)は、適切なインデックス作成、動作する外部キー、データベースツールが検査できるスキーマ、フィールドごとのJSON解析不要を提供します。
責務は分離されたままです:
| 責務 | 場所 | テーブル |
|---|---|---|
| スキーマ | システムテーブル | _emdash_collections, _emdash_fields |
| コンテンツ | コレクションごとのテーブル | ec_posts, ec_products, … |
| メディア | 別テーブル + ストレージ | mediaテーブル + R2/S3 |
| 設定 | オプションテーブル | site:プレフィックス付きoptions |
ランタイムスキーマ変更
管理UIでフィールドを追加すると3つのステップが実行されます:
_emdash_fieldsにレコードを挿入する。ALTER TABLE ec_<collection> ADD COLUMN <name> <TYPE>を実行する。- バリデーションに使用するZodスキーマを再生成する。
SQLiteはランタイムでカラムの追加、リネーム、ドロップ(ドロップにはSQLite 3.35+が必要)をサポートしています。カラムの型変更は直接サポートされていないため、EmDashはテーブルを透過的に再構築します:新しいテーブルを作成し、行をコピーし、古いテーブルを削除し、新しいテーブルをリネームします。
ランタイムバリデーション
EmDashは起動時にフィールド定義からZodスキーマを構築し、すべての作成と更新をそれに対して検証します:
function buildSchema(fields: Field[]): ZodSchema {
const shape: Record<string, ZodType> = {};
for (const field of fields) {
let zodType = fieldTypeToZod(field.type);
if (field.required) zodType = zodType.required();
if (field.validation?.min !== undefined) zodType = zodType.min(field.validation.min);
shape[field.slug] = zodType;
}
return z.object(shape);
}
データレイヤー
EmDashはサポートされるすべてのデータベース(SQLite、libSQL、Cloudflare D1、PostgreSQL)にわたって型安全なSQLのためにKyselyを使用します。ダイアレクトはサイトが統合に渡す設定からvirtual:emdash/dialectによって選択されます。
Live Collectionsローダー
コンテンツはAstroのLive Collectionsを通じてランタイムで提供されます。emdashLoader()はAstroのLiveLoaderインターフェースを実装し、単一の_emdashコレクションとして登録されます:
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({ loader: emdashLoader() }),
};
単一の_emdashコレクションがすべてのコンテンツタイプをラップし、getEmDashCollection("posts")が呼び出されるとローダーがタイプでフィルタリングします。
リクエストパス
ページからのコンテンツリクエスト:
- Astroがリクエストを受信し、ページコンポーネントを実行する。
getEmDashCollection()がAstroのgetLiveCollection()を呼び出す。emdashLoaderがKyselyを通じて関連するec_*テーブルをクエリする。- 行がAstroのエントリフォーマット(
id、slug、data)にマッピングされる。 - コンポーネントがレンダリングする。
管理リクエスト:
- ミドルウェアがセッショントークンを検証する。
- APIルートがリポジトリを通じてCRUDを実行する。
- ライフサイクルフックが発火する(例:
content:beforeSave)。 - KyselyがSQLを実行する。
- ルートがAdmin SPAにJSONを返す。
管理パネルの内部
管理パネルは1つのReactアイランドです。Astroがシェルを提供しミドルウェアで認証を強制します。内部はすべてクライアントサイドで、TanStack Router、TanStack Query、TanStack Table、React Hook Form + Zod、TipTap、Kumo(CloudflareのBase UI + Tailwindデザインシステム)で構築されています。
シェルルートがミドルウェアでアクセスを制御します:
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();
}
マニフェスト駆動UI
管理パネルはコレクションやプラグインについて何もハードコードしていません。GET /_emdash/api/manifestを取得し、リクエストユーザーがアクセスできるコレクション、プラグイン、タクソノミーをロールでフィルタリングして返します:
{
"collections": [
{
"slug": "posts",
"label": "Blog Posts",
"icon": "file-text",
"supports": ["drafts", "revisions", "preview"],
"fields": [{ "slug": "title", "type": "string", "required": true }]
}
],
"plugins": [{ "id": "audit-log", "label": "Audit Log" }],
"taxonomies": [{ "name": "category", "label": "Categories", "hierarchical": true }],
"version": "abc123"
}
ナビゲーション、フォーム、フィールドエディターはこのマニフェストから生成されるため、スキーマとプラグインの変更は管理パネルのリビルドなしに表示され、Zodスキーマはサーバーサイドに留まります。
プラグイン管理UI
プラグインの管理エントリポイントは、バンドラーが解決してツリーシェイクできるように、生成された仮想モジュールの静的インポートに収集されます:
import * as pluginAdmin0 from "@emdash-cms/plugin-seo/admin";
export const pluginAdmins = { seo: pluginAdmin0 };
リッチテキスト変換
Portable TextフィールドはTipTap(ProseMirror)で編集されます。コンテンツはロードとセーブの境界でportableTextToProsemirror()とprosemirrorToPortableText()によって変換されます。プラグインやインポートからの不明なブロックは読み取り専用プレースホルダーとして保持されます。
署名付きアップロード
メディアアップロードはアダプターがサポートする場合はストレージ直接署名付きURLを使用し、それ以外の場合はセイムオリジンストリーミングエンドポイントを使用します:
- クライアントがアップロードURLを要求する(
POST /api/media/upload-url)。 - クライアントが返されたターゲットにアップロードする。S3互換アダプターはWorkerのボディサイズ制限を回避する署名付きURLを返すことができます。ネイティブR2バインディングとローカルストレージはEmDashストリーミングエンドポイントを返します。
- クライアントが確認する(
POST /api/media/:id/confirm)。 - サーバーがメタデータ(寸法、MIMEタイプ)を抽出する。
コンテンツインポーターの拡張
WordPressインポーターはプラグ可能なImportSourceインターフェースで構築されています。カスタムソースはprobe、analyze、fetchを実装します:
interface ImportSource {
probe(input: ImportInput): Promise<ProbeResult>;
analyze(input: ImportInput): Promise<AnalysisResult>;
fetchContent(input: ImportInput): AsyncIterable<NormalizedEntry>;
}
probeは入力を検証し見つかったものを報告し、analyzeはソースのポストタイプをEmDashコレクションにマッピングしスキーマのギャップをフラグし、fetchContentは管理パネルが使用するのと同じリポジトリを通じてインポートパイプラインが書き込む正規化されたエントリをストリームします。組み込みソースはWordPress WXR、WordPress.com、WordPress REST APIをカバーしています。別のシステムからインポートするにはカスタムソースを登録してください。