Diese Seite ist für Personen, die an EmDash arbeiten, nicht für diejenigen, die eine Website damit erstellen. Sie erklärt das Datenbank-Layout, die Astro-Integration, Anfragepfade, die Admin-Anwendung, den Medienfluss und das Import-System. 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-Anwendung und REST-API-Routen mit Astros
injectRoute-API. Nichts wird in das Benutzerprojekt kopiert. Die wichtigsten Routenfamilien sind:Pfadmuster Zweck /_emdash/admin/[...path]Admin-Panel-SPA /_emdash/api/manifestAdmin-Manifest (Sammlungen, Plugins) /_emdash/api/content/[collection]/...Inhaltseintrag-Operationen /_emdash/api/media/...Medienbibliothek-Operationen /_emdash/api/schema/...Schema-Verwaltung /_emdash/api/settings/...Website-Einstellungen /_emdash/api/menus/...Navigationsmenüs /_emdash/api/taxonomies/...Kategorien, Tags, benutzerdefinierte Taxonomien /_emdash/api/plugins/[pluginId]/[...path]Plugin-definierte API-Routen Der Route-Injector ist das vollständige Inventar, einschließlich Authentifizierung, Kommentare, Suche, Importe, Widgets und anderer Routenfamilien.
-
Generiert virtuelle Module, damit der Bundler Konfigurations- und Erweiterungscode auflösen kann:
Modul Zweck virtual:emdash/configDatenbank-, Speicher- und Website-Konfiguration virtual:emdash/dialectDatenbankdialekt-Factory virtual:emdash/admin-registryStatische Importe für Plugin-Admin-Interfaces virtual:emdash/pluginsKonfigurierte Plugin-Implementierungen virtual:emdash/media-providersKonfigurierte externe Medienanbieter virtual-modules.tsdefiniert die restlichen Runtime-Helfer und generierten Modulinhalte. -
Stellt den Live Content Collections Loader bereit und registriert die Runtime-Middleware. Zur Anfragezeit öffnet die Middleware die konfigurierten Datenbank- und Speicherverbindungen und wendet ausstehende Migrationen an, bevor Routen sie verwenden.
Datenbank-zuerst-Schema
Schemadefinitionen befinden sich in der Datenbank, nicht in einer statischen Konfigurationsdatei. _emdash_collections speichert eine Zeile pro Sammlung. Die Kernspalten beschreiben die Sammlung und die Funktionen, die Runtime und Admin bereitstellen:
| Spalten | Zweck |
|---|---|
id, slug | Stabile Sammlungsidentität |
label, label_singular, description, icon | Namen und Anleitungen für Redakteure |
supports, has_seo, comments_enabled, edit_locking | Optionale Sammlungsfähigkeiten |
title_field, date_field, admin_config, hidden, sort_order | Admin-Liste und Navigationsverhalten |
url_pattern, routable | Öffentliche URL und Slug-Verhalten |
source | Wie die Sammlung erstellt wurde |
Der source-Wert zeichnet die Herkunft auf, wie manual, seed, template:<name>, import:<name> oder discovered. Zusätzliche Einstellungen stammen aus registrierten Migrationen, daher sind database/types.ts und die Migrationen das aktuelle Spalteninventar.
_emdash_fields speichert die Felder, die mit jeder Sammlung verknüpft sind:
| Spalten | Zweck |
|---|---|
id, collection_id, slug | Feldidentität und zugehörige Sammlung |
label, type, column_type | Editor-Label, EmDash-Feldtyp und SQL-Speichertyp |
required, unique, default_value, validation | Inhaltseinschränkungen und Standards |
widget, options, sort_order | Editor-Steuerung und Anzeigereihenfolge |
searchable, indexed, translatable | Such-, Abfrage- und Lokalisierungsverhalten |
collection_id referenziert _emdash_collections.id, und jeder Feld-Slug ist innerhalb seiner Sammlung eindeutig.
Inhaltstabellen pro Sammlung
Jede Sammlung erhält ihre eigene Tabelle mit dem Präfix ec_. Eine products-Sammlung mit den Feldern title und price erzeugt eine Tabelle mit dieser Form:
CREATE TABLE ec_products (
-- Systemspalten, in jeder Inhaltstabelle vorhanden
id TEXT PRIMARY KEY,
slug TEXT,
status TEXT DEFAULT 'draft',
author_id TEXT,
primary_byline_id TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
published_at TEXT,
scheduled_at TEXT,
deleted_at TEXT,
version INTEGER DEFAULT 1,
live_revision_id TEXT,
draft_revision_id TEXT,
locale TEXT NOT NULL DEFAULT 'en',
translation_group TEXT,
-- Inhaltsspalten, aus Felddefinitionen erstellt
title TEXT NOT NULL,
price REAL,
UNIQUE (slug, locale)
);
Echte Spalten geben jedem Feld einen Datenbanktyp, ermöglichen Indizes und Fremdschlüssel und lassen Datenbanktools das Schema inspizieren, ohne einen Inhalts-JSON-Blob zu decodieren. Die Unique-Constraint erlaubt es Übersetzungen, einen Slug zu teilen, während jeder Slug innerhalb einer Sprache eindeutig bleibt. Alle Sprachvarianten desselben Eintrags teilen einen translation_group-Wert, der EmDash die Zeilen finden lässt, die Übersetzungen voneinander sind.
Die Hauptdatenbelange bleiben getrennt:
| Belang | Ort | Tabellen |
|---|---|---|
| Schema | Systemtabellen | _emdash_collections, _emdash_fields |
| Inhalt | Tabellen pro Sammlung | ec_posts, ec_products, … |
| Medien | Separate Tabelle + Speicher | media-Tabelle + konfigurierter Speicher |
| Einstellungen | Optionstabelle | options mit site:-Präfix |
Runtime-Schemaänderungen
Das Hinzufügen eines Feldes über die Admin-UI führt diese Schritte aus:
- Die Felddefinition in
_emdash_fieldseinfügen. - Die entsprechende Spalte zur
ec_*-Tabelle der Sammlung hinzufügen und einen Index erstellen, wenn das Feld als indexiert konfiguriert ist. - Generierte Entwicklungstypen aktualisieren, damit das neue Feld in der Editor-Werkzeugunterstützung erscheint.
Die Inhaltsvalidierung liest die aktuellen Felddefinitionen und erstellt ein Zod-Schema, wenn Inhalte erstellt oder aktualisiert werden. Das Ändern des zugrunde liegenden SQL-Typs, der required- oder unique-Einschränkung oder des Lokalisierungsverhaltens eines Feldes kann eine manuelle Inhaltsmigration erfordern; SchemaRegistry lehnt nicht unterstützte In-Place-Änderungen ab, anstatt die Tabelle implizit neu aufzubauen.
Runtime-Validierung
EmDash leitet ein Zod-Schema aus den aktuellen Feldern der Sammlung ab. Der Generator delegiert die Typ- und Einschränkungsdetails an generateFieldSchema():
export function generateZodSchema(
collection: CollectionWithFields,
): z.ZodObject<Record<string, ZodType>> {
const shape: Record<string, ZodType> = {};
for (const field of collection.fields) {
shape[field.slug] = generateFieldSchema(field);
}
return z.object(shape);
}
Der Inhalts-Handler lehnt auch unbekannte Felder ab, prüft erforderliche String-Werte und verifiziert Referenzen zu anderen Sammlungen.
Datenschicht
EmDash verwendet Kysely für typisiertes SQL über SQLite, libSQL, Cloudflare D1 und PostgreSQL. Die Website-Konfiguration wählt den Datenbankadapter; die Integration stellt ihre Dialekt-Factory über virtual:emdash/dialect bereit.
Live Content Collections Loader
Inhalte werden zur Laufzeit über Astros Live Content 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 umhüllt jede EmDash-Sammlung. getEmDashCollection("posts") liefert den posts-Typfilter, und der Loader bildet ihn auf die ec_posts-Tabelle ab.
Anfragepfade
Eine Inhaltsanfrage von einer Astro-Seite folgt diesem Pfad:
- Die Seite ruft
getEmDashCollection()odergetEmDashEntry()auf. - Der Abfrage-Wrapper ruft Astros
getLiveCollection()odergetLiveEntry()mit der internen_emdash-Sammlung und dem angeforderten EmDash-Sammlungstyp auf. emdashLoader()fragt die relevanteec_*-Tabelle über Kysely ab und wendet Veröffentlichungs-, Sprach-, Filter-, Sortier- und Paginierungsregeln an.- Der Abfrage-Wrapper bildet Zeilen auf Astro-Einträge ab und lädt deren Autorennamen und Taxonomie-Begriffe.
- Die Astro-Komponente rendert die zurückgegebenen Einträge.
Vorschau- und Bearbeitungsmodus-Status reisen durch den Anfrage-Kontext, sodass dieselben Abfragefunktionen Entwurfsinhalte zurückgeben können, nachdem die Middleware die Anfrage verifiziert hat.
Eine Admin-API-Anfrage folgt einem separaten Pfad:
- Die Middleware authentifiziert die Anfrage und speichert den aufgelösten Benutzer in
Astro.locals. - Die API-Route parst die Anfrage und prüft die für diese Operation benötigte Berechtigung.
- Die Route delegiert die Geschäftslogik an einen Handler oder ein Repository.
- Der Handler führt Plugin-Lifecycle-Hooks rund um die Datenbankoperation aus, wenn diese Operation Hooks bereitstellt.
- Die Route gibt eine Standard-JSON-Erfolgs- oder Fehlerantwort an die Admin-Anwendung zurück.
Admin-Panel-Interna
Das Admin-Panel ist eine React Single-Page-Application. Astro liefert seine Shell und die Authentifizierungs-Middleware schützt Admin-Routen. Innerhalb der Anwendung übernimmt TanStack Router die Navigation, TanStack Query lädt den Server-Zustand, TanStack Table rendert Datentabellen, React Hook Form und Zod verwalten Formulare, TipTap bearbeitet Portable Text und Kumo liefert das Designsystem.
Für die Session-Authentifizierung leitet die Middleware eine nicht authentifizierte Browser-Anfrage zur Login-Seite um und gibt einen JSON-Fehler für eine nicht authentifizierte API-Anfrage zurück. Nachdem ein aktiver Benutzer geladen wurde, platziert sie diesen auf Astro.locals für die Route:
const sessionUser = await resolveSessionUser(session);
if (!sessionUser?.id) {
if (isApiRoute) {
return apiError("NOT_AUTHENTICATED", "Not authenticated", 401);
}
const loginUrl = new URL("/_emdash/admin/login", getPublicOrigin(url, emdash?.config));
loginUrl.searchParams.set("redirect", url.pathname);
return context.redirect(loginUrl.toString());
}
Nach dieser Verzweigung lädt die Middleware den Benutzer, lehnt fehlende oder deaktivierte Konten ab, platziert den aktiven Benutzer auf Astro.locals und fährt mit der Route fort.
Manifest-gesteuertes UI
Das Admin-Panel kodiert keine Sammlungsschemata oder Plugin-Beiträge fest. Es ruft GET /_emdash/api/manifest ab, das die aktuellen Sammlungen, Felder, Plugins, Taxonomien, den Authentifizierungsmodus und andere konfigurierte Fähigkeiten beschreibt. Ein gekürztes Manifest sieht so aus:
{
"collections": {
"posts": {
"label": "Blog Posts",
"labelSingular": "Post",
"supports": ["drafts", "revisions", "preview"],
"fields": {
"title": { "kind": "string", "label": "Title", "required": true }
}
}
},
"plugins": {
"audit-log": { "version": "0.2.1", "enabled": true }
},
"taxonomies": [
{ "name": "category", "label": "Categories", "hierarchical": true }
],
"version": "0.37.0"
}
Das Admin-Panel verwendet das Manifest, um Sammlungsnavigation und Feldeditoren zu erstellen. Da der Endpunkt das Live-Schema liest, erscheinen Sammlungs- und Feldänderungen, ohne die Admin-Anwendung neu zu erstellen.
Plugin-Admin-UIs
Konfigurierte Plugin-Admin-Einstiegspunkte werden in virtual:emdash/admin-registry gesammelt. Das generierte Modul verwendet statische Importe, damit der Bundler die React-Komponenten einschließen kann:
import * as pluginAdmin0 from "@emdash-cms/plugin-seo/admin";
export const pluginAdmins = { seo: pluginAdmin0 };
Rich-Text-Konvertierung
Portable-Text-Felder verwenden TipTap, das auf ProseMirror basiert. EmDash konvertiert Portable Text zu ProseMirror, wenn der Editor lädt, und konvertiert es zurück zu Portable Text, wenn der Eintrag gespeichert wird. Unbekannte Blöcke von Plugins oder Importen werden als schreibgeschützte Platzhalter beibehalten, anstatt verworfen zu werden.
Signierte Uploads
Medien-Uploads verwenden direkt-zum-Speicher signierte URLs, wenn der Speicheradapter sie unterstützt, und andernfalls einen Same-Origin-Streaming-Endpunkt:
- Der Client fordert ein Upload-Ziel von
POST /_emdash/api/media/upload-urlan. EmDash erstellt ein ausstehendes Medienelement. - Der Client lädt zum zurückgegebenen Ziel hoch. S3-kompatible Adapter können eine signierte URL zurückgeben, die Anwendungs-Größenlimits umgeht; native R2-Bindings und lokaler Speicher geben einen EmDash-Streaming-Endpunkt zurück.
- Der Client bestätigt den Upload mit
POST /_emdash/api/media/:id/confirm. - EmDash validiert die gespeicherte Datei und markiert das Medienelement als bereit.
Erweiterung des Inhaltsimporters
Der WordPress-Importer verwendet ein erweiterbares ImportSource-Interface. Eine Quelle kann eine URL untersuchen, den verfügbaren Inhalt gegen das aktuelle Schema analysieren und normalisierte Inhaltselemente streamen:
interface ImportSource {
id: string;
name: string;
description: string;
icon: "upload" | "globe" | "wordpress" | "plug";
requiresFile?: boolean;
canProbe?: boolean;
probe?(url: string): Promise<SourceProbeResult | null>;
analyze(input: SourceInput, context: ImportContext): Promise<ImportAnalysis>;
fetchContent(input: SourceInput, options: FetchOptions): AsyncGenerator<NormalizedItem>;
fetchMedia?(url: string, input: SourceInput): Promise<Blob>;
}
Die WXR-Quelle importiert WordPress-Exportdateien. Die Connector-Quelle importiert direkt von Websites mit dem EmDash-WordPress-Plugin. Eine separate REST-Quelle erkennt öffentliche WordPress-Websites, leitet den Benutzer aber zu einem WXR-Export weiter, da direkter REST-Import nicht implementiert ist. Registrieren Sie eine weitere Quelle, wenn ein Importer die gleichen normalisierten Analyse- und Inhaltselement-Formen erzeugen kann.