Diese Seite ist für Personen, die an EmDash arbeiten, nicht für diejenigen, die eine Website damit erstellen. Sie dokumentiert interne Mechanismen — Tabellenlayouts, die Astro-Integration, den Request-Pfad, Code-Generierung. Nichts davon wird benötigt, um EmDash zu verwenden. Wenn Sie eine Website erstellen, lesen Sie stattdessen Architektur und das Inhaltsmodell.
Die Astro-Integration
EmDash läuft als Astro-Integration aus dem emdash-Paket. Zur Build-Zeit:
-
Injiziert es die Admin-SPA und REST-API-Routen mit Astros
injectRoute-API. Nichts wird in das Benutzerprojekt kopiert. Die injizierten Pfade sind:Pfadmuster Zweck /_emdash/admin/[...path]Admin-Panel SPA /_emdash/api/manifestAdmin-Manifest (Sammlungen, Plugins) /_emdash/api/content/[collection]Content-Eintrags-CRUD /_emdash/api/media/*Medienbibliothek-Operationen /_emdash/api/schema/*Schema-Verwaltung /_emdash/api/settingsWebsite-Einstellungen /_emdash/api/menus/*Navigationsmenüs /_emdash/api/taxonomies/*Kategorien, Tags, benutzerdefinierte Taxonomien -
Generiert virtuelle Module, damit der Bundler Konfigurations- und Plugin-Code auflösen und tree-shaken kann:
Modul Zweck virtual:emdash/configDatenbank- und Speicherkonfiguration virtual:emdash/dialectDatenbank-Dialekt-Factory virtual:emdash/plugin-adminsStatische Imports für Plugin-Admin-UIs -
Stellt den Live-Collections-Loader bereit, verwaltet Migrationen und öffnet die Speicherverbindung.
Datenbank-First-Schema
Schema-Definitionen leben in der Datenbank, nicht im Code. Zwei Systemtabellen verfolgen die Struktur.
_emdash_collections enthält eine Zeile pro Sammlung:
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, -- wie sie erstellt wurde
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT
);
Die source-Spalte zeichnet die Herkunft auf: manual (Admin-UI), template:<name> (Seed-Datei), import:wordpress (Importer) oder discovered (automatisch aus bestehenden Tabellen erkannt).
_emdash_fields enthält eine Zeile pro Feld, verknüpft mit seiner Sammlung:
CREATE TABLE _emdash_fields (
id TEXT PRIMARY KEY,
collection_id TEXT REFERENCES _emdash_collections(id),
slug TEXT NOT NULL, -- Spaltenname
label TEXT NOT NULL,
type TEXT NOT NULL, -- Feldtyp
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)
);
Inhaltstabellen pro Sammlung
Jede Sammlung bekommt ihre eigene Tabelle, mit dem Präfix ec_. Eine products-Sammlung mit title- und price-Feldern erzeugt:
CREATE TABLE ec_products (
-- Systemspalten, immer vorhanden
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, -- Soft Delete
version INTEGER DEFAULT 1, -- Optimistisches Locking
-- Inhaltsspalten, aus Felddefinitionen
title TEXT NOT NULL,
price REAL
);
Echte Spalten (statt einer Tabelle mit einem JSON-Blob) ermöglichen ordnungsgemäße Indizierung, funktionierende Fremdschlüssel, ein Schema, das Datenbanktools inspizieren können, und kein JSON-Parsing pro Feld.
Die Zuständigkeiten bleiben getrennt:
| Zuständigkeit | Ort | Tabellen |
|---|---|---|
| Schema | Systemtabellen | _emdash_collections, _emdash_fields |
| Inhalt | Pro-Sammlung-Tabellen | ec_posts, ec_products, … |
| Medien | Separate Tabelle + Speicher | media-Tabelle + R2/S3 |
| Einstellungen | Options-Tabelle | options mit site:-Präfix |
Schema-Änderungen zur Laufzeit
Das Hinzufügen eines Feldes über die Admin-UI führt drei Schritte aus:
- Einen Datensatz in
_emdash_fieldseinfügen. ALTER TABLE ec_<collection> ADD COLUMN <name> <TYPE>ausführen.- Das für die Validierung verwendete Zod-Schema neu generieren.
SQLite unterstützt zur Laufzeit das Hinzufügen, Umbenennen und Löschen von Spalten (Löschen erfordert SQLite 3.35+). Das Ändern des Typs einer Spalte wird nicht direkt unterstützt, daher baut EmDash die Tabelle transparent neu auf: neue Tabelle erstellen, Zeilen kopieren, alte Tabelle löschen, neue umbenennen.
Laufzeit-Validierung
EmDash erstellt beim Start Zod-Schemas aus den Felddefinitionen und validiert jeden Erstellungs- und Aktualisierungsvorgang dagegen:
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);
}
Datenschicht
EmDash verwendet Kysely für typsicheres SQL über alle unterstützten Datenbanken hinweg (SQLite, libSQL, Cloudflare D1 und PostgreSQL). Der Dialekt wird von virtual:emdash/dialect aus der Konfiguration ausgewählt, die die Website an die Integration übergibt.
Live-Collections-Loader
Inhalte werden zur Laufzeit über Astros Live Collections bereitgestellt. emdashLoader() implementiert Astros LiveLoader-Interface und wird als einzelne _emdash-Sammlung registriert:
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({ loader: emdashLoader() }),
};
Die einzelne _emdash-Sammlung umfasst jeden Inhaltstyp; der Loader filtert nach Typ, wenn getEmDashCollection("posts") aufgerufen wird.
Request-Pfade
Ein Content-Request von einer Seite:
- Astro empfängt den Request und führt die Seitenkomponente aus.
getEmDashCollection()ruft AstrosgetLiveCollection()auf.emdashLoaderfragt die relevanteec_*-Tabelle über Kysely ab.- Zeilen werden in Astros Entry-Format (
id,slug,data) umgewandelt. - Die Komponente rendert.
Ein Admin-Request:
- Middleware validiert das Session-Token.
- Die API-Route führt CRUD über ein Repository aus.
- Lifecycle-Hooks feuern (zum Beispiel
content:beforeSave). - Kysely führt das SQL aus.
- Die Route gibt JSON an die Admin-SPA zurück.
Admin-Panel-Internes
Das Admin-Panel ist eine React-Island. Astro liefert die Shell und erzwingt Authentifizierung in der Middleware; alles darin ist clientseitig, aufgebaut auf TanStack Router, TanStack Query, TanStack Table, React Hook Form + Zod, TipTap und Kumo (Cloudflares Base UI + Tailwind Design-System).
Die Shell-Route kontrolliert den Zugang in der Middleware:
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();
}
Manifest-getriebene UI
Das Admin-Panel hardcodiert nichts über Sammlungen oder Plugins. Es ruft GET /_emdash/api/manifest ab, das die Sammlungen, Plugins und Taxonomien zurückgibt, auf die der anfragende Benutzer zugreifen darf, gefiltert nach Rolle:
{
"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"
}
Navigation, Formulare und Feld-Editoren werden aus diesem Manifest generiert, sodass Schema- und Plugin-Änderungen ohne Admin-Rebuild erscheinen und Zod-Schemas serverseitig bleiben.
Plugin-Admin-UIs
Plugin-Admin-Einstiegspunkte werden in einem generierten virtuellen Modul statischer Imports gesammelt, damit der Bundler sie auflösen und tree-shaken kann:
import * as pluginAdmin0 from "@emdash-cms/plugin-seo/admin";
export const pluginAdmins = { seo: pluginAdmin0 };
Rich-Text-Konvertierung
Portable-Text-Felder werden in TipTap (ProseMirror) bearbeitet. Inhalte werden an den Lade- und Speichergrenzen durch portableTextToProsemirror() und prosemirrorToPortableText() konvertiert. Unbekannte Blöcke von Plugins oder Importen werden als schreibgeschützte Platzhalter beibehalten.
Signierte Uploads
Medien-Uploads verwenden direkte Storage-signierte URLs, wenn der Adapter sie unterstützt, und andernfalls einen Same-Origin-Streaming-Endpoint:
- Der Client fordert eine Upload-URL an (
POST /api/media/upload-url). - Der Client lädt zum zurückgegebenen Ziel hoch. S3-kompatible Adapter können eine signierte URL zurückgeben, die Worker-Body-Size-Limits umgeht; native R2-Bindings und lokaler Speicher geben einen EmDash-Streaming-Endpoint zurück.
- Der Client bestätigt (
POST /api/media/:id/confirm). - Der Server extrahiert Metadaten (Abmessungen, MIME-Typ).
Erweiterung des Content-Importers
Der WordPress-Importer basiert auf einem plugfähigen ImportSource-Interface. Eine benutzerdefinierte Quelle implementiert probe, analyze und fetch:
interface ImportSource {
probe(input: ImportInput): Promise<ProbeResult>;
analyze(input: ImportInput): Promise<AnalysisResult>;
fetchContent(input: ImportInput): AsyncIterable<NormalizedEntry>;
}
probe validiert die Eingabe und meldet, was gefunden wurde, analyze ordnet Quell-Post-Typen EmDash-Sammlungen zu und markiert Schema-Lücken, und fetchContent streamt normalisierte Einträge, die die Import-Pipeline über dieselben Repositories schreibt, die das Admin-Panel verwendet. Eingebaute Quellen decken WordPress WXR, WordPress.com und die WordPress-REST-API ab; registrieren Sie eine benutzerdefinierte Quelle, um aus einem anderen System zu importieren.