Architektur (Interna)

Auf dieser Seite

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:

    PfadmusterZweck
    /_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:

    ModulZweck
    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.ts definiert 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:

SpaltenZweck
id, slugStabile Sammlungsidentität
label, label_singular, description, iconNamen und Anleitungen für Redakteure
supports, has_seo, comments_enabled, edit_lockingOptionale Sammlungsfähigkeiten
title_field, date_field, admin_config, hidden, sort_orderAdmin-Liste und Navigationsverhalten
url_pattern, routableÖffentliche URL und Slug-Verhalten
sourceWie 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:

SpaltenZweck
id, collection_id, slugFeldidentität und zugehörige Sammlung
label, type, column_typeEditor-Label, EmDash-Feldtyp und SQL-Speichertyp
required, unique, default_value, validationInhaltseinschränkungen und Standards
widget, options, sort_orderEditor-Steuerung und Anzeigereihenfolge
searchable, indexed, translatableSuch-, 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:

BelangOrtTabellen
SchemaSystemtabellen_emdash_collections, _emdash_fields
InhaltTabellen pro Sammlungec_posts, ec_products, …
MedienSeparate Tabelle + Speichermedia-Tabelle + konfigurierter Speicher
EinstellungenOptionstabelleoptions mit site:-Präfix

Runtime-Schemaänderungen

Das Hinzufügen eines Feldes über die Admin-UI führt diese Schritte aus:

  1. Die Felddefinition in _emdash_fields einfügen.
  2. Die entsprechende Spalte zur ec_*-Tabelle der Sammlung hinzufügen und einen Index erstellen, wenn das Feld als indexiert konfiguriert ist.
  3. 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:

  1. Die Seite ruft getEmDashCollection() oder getEmDashEntry() auf.
  2. Der Abfrage-Wrapper ruft Astros getLiveCollection() oder getLiveEntry() mit der internen _emdash-Sammlung und dem angeforderten EmDash-Sammlungstyp auf.
  3. emdashLoader() fragt die relevante ec_*-Tabelle über Kysely ab und wendet Veröffentlichungs-, Sprach-, Filter-, Sortier- und Paginierungsregeln an.
  4. Der Abfrage-Wrapper bildet Zeilen auf Astro-Einträge ab und lädt deren Autorennamen und Taxonomie-Begriffe.
  5. 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:

  1. Die Middleware authentifiziert die Anfrage und speichert den aufgelösten Benutzer in Astro.locals.
  2. Die API-Route parst die Anfrage und prüft die für diese Operation benötigte Berechtigung.
  3. Die Route delegiert die Geschäftslogik an einen Handler oder ein Repository.
  4. Der Handler führt Plugin-Lifecycle-Hooks rund um die Datenbankoperation aus, wenn diese Operation Hooks bereitstellt.
  5. 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:

  1. Der Client fordert ein Upload-Ziel von POST /_emdash/api/media/upload-url an. EmDash erstellt ein ausstehendes Medienelement.
  2. 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.
  3. Der Client bestätigt den Upload mit POST /_emdash/api/media/:id/confirm.
  4. 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.