Zum Inhalt springen

Admin-Oberfläche

Das EmDash-Admin-Oberfläche ist eine React-Single-Page-Anwendung, die in Ihre Astro-Website eingebettet ist. Es bietet eine vollständige Inhaltsverwaltungsoberfläche für Redakteure und Administratoren.

┌────────────────────────────────────────────────────────────────┐
│ Astro Shell │
│ /_emdash/admin/[...path].astro │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ React SPA │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │
│ │ │ TanStack │ │ TanStack │ │ Kumo │ │ │
│ │ │ Router │ │ Query │ │ Components │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ REST API Client │ │ │
│ │ │ /_emdash/api/* │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘

Der Admin ist eine “große Insel”-React-App. Astro übernimmt das Grundgerüst und die Authentifizierung; die gesamte Navigation und das Rendering innerhalb des Admins erfolgen clientseitig.

EbeneTechnologieZweck
RoutingTanStack RouterTypsicheres clientseitiges Routing
DatenTanStack QueryServerzustand, Caching, Mutationen
UIKumoBarrierefreie Komponenten (Base UI + Tailwind)
TabellenTanStack TableSortieren, Filtern, Paginierung
FormulareReact Hook Form + ZodValidierung entsprechend Serverschema
IconsPhosphorKonsistente Ikonografie
EditorTipTapRich-Text-Editierung (Portable Text)

Der Admin wird unter /_emdash/admin/ eingebunden und nutzt clientseitiges Routing:

PfadBildschirm
/Dashboard
/content/:collectionInhaltsliste
/content/:collection/:idInhaltseditor
/content/:collection/newNeuer Eintrag
/mediaMedienbibliothek
/content-typesSchema-Builder (nur Admin)
/menusNavigationsmenüs
/widgetsWidget-Bereiche
/taxonomiesKategorie-/Tag-Verwaltung
/settingsWebsite-Einstellungen
/plugins/:pluginId/*Plugin-Seiten

Der Admin kodiert kein Wissen über Sammlungen oder Plugins fest ein. Stattdessen ruft er ein Manifest vom Server ab:

GET /_emdash/api/manifest

Antwort:

{
"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": "Pruefprotokoll",
"adminPages": [{ "path": "history", "label": "Pruefverlauf" }],
"widgets": [{ "id": "recent-activity", "title": "Letzte Aktivitaet" }]
}
],
"taxonomies": [{ "name": "category", "label": "Kategorien", "hierarchical": true }],
"version": "abc123"
}

Der Admin baut seine Navigation, Formulare und Editoren vollständig aus diesem Manifest auf. Vorteile:

  • Schemaänderungen erscheinen sofort — Kein Neubuild des Admins erforderlich
  • Plugin-UI integriert sich automatisch — Seiten und Widgets aus dem Manifest
  • Typsicherheit an der Grenze — Zod-Schemata bleiben auf dem Server
  1. Admin-SPA lädt — TanStack Router initialisiert 2. Manifest abrufen — TanStack Query cached Sammlungs-/Plugin-Metadaten 3. Navigation aufbauen — Seitenleiste aus Manifest generiert 4. Benutzer navigiert — Clientseitiges Routing, kein Seitenneuladen 5. Daten abrufen — TanStack Query fordert Inhalte von REST-APIs an 6. Formulare rendern — Feld-Editoren aus Manifest-Feldbeschreibungen generiert 7. Änderungen senden — Mutationen über TanStack Query, optimistische Updates 8. Server validiert — Zod-Schemata auf dem Server, Fehler als JSON zurückgegeben

Der Admin kommuniziert ausschließlich über REST-APIs:

MethodeEndpunktZweck
GET/api/content/:collectionEinträge auflisten
POST/api/content/:collectionEintrag erstellen
GET/api/content/:collection/:idEintrag abrufen
PUT/api/content/:collection/:idEintrag aktualisieren
DELETE/api/content/:collection/:idEintrag soft löschen
GET/api/content/:collection/:id/revisionsVersionen auflisten
POST/api/content/:collection/:id/preview-urlVorschau-URL generieren
MethodeEndpunktZweck
GET/api/schemaVollständiges Schema exportieren
GET/api/schema/collectionsSammlungen auflisten
POST/api/schema/collectionsSammlung erstellen
PUT/api/schema/collections/:slugSammlung aktualisieren
DELETE/api/schema/collections/:slugSammlung löschen
POST/api/schema/collections/:slug/fieldsFeld hinzufügen
PUT/api/schema/collections/:slug/fields/:fieldFeld aktualisieren
DELETE/api/schema/collections/:slug/fields/:fieldFeld löschen
MethodeEndpunktZweck
GET/api/mediaMedienobjekte auflisten
POST/api/media/upload-urlSignierte Upload-URL abrufen
POST/api/media/:id/confirmUpload bestätigen
DELETE/api/media/:idMedienobjekt löschen
GET/api/media/file/:keyMediendatei ausliefern
EndpunktZweck
/api/settingsWebsite-Einstellungen (GET/POST)
/api/menus/*Navigationsmenüs
/api/widget-areas/*Widget-Verwaltung
/api/taxonomies/*Taxonomie-Begriffe
/api/admin/plugins/*Plugin-Zustand

Alle Listen-Endpunkte nutzen cursor-basierte Paginierung:

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

Nächste Seite abrufen:

GET /api/content/posts?cursor=eyJpZCI6IjAxSjEyMzQ1NiJ9

Plugins können das Admin-Interface mit Seiten und Dashboard-Widgets erweitern. Die Integration generiert ein virtuelles Modul mit statischen Imports:

// virtual:emdash/plugin-admins (generated)
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,
};

Plugin-Seiten werden unter /_emdash/admin/plugins/:pluginId/* eingebunden:

// @emdash-cms/plugin-seo/src/admin.tsx
export const pages = [
{
path: "settings",
component: SEOSettingsPage,
label: "SEO Settings",
},
];

Wird gerendert unter: /_emdash/admin/plugins/seo/settings

Plugins können Widgets zum Dashboard hinzufügen:

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

Die Admin-Shell-Route erzwingt die Authentifizierung über Astro-Middleware:

// Simplified middleware logic
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();
}

Die Admin-SPA selbst verarbeitet keine Anmeldung – das ist eine Astro-Seite, die ein Session-Cookie setzt.

Verschiedene Rollen sehen unterschiedliche Teile des Admin-Bereichs:

RolleSichtbare Abschnitte
EditorDashboard, zugewiesene Sammlungen, Medien
Admin+ Inhaltstypen, alle Sammlungen, Einstellungen
Entwickler+ CLI-Zugriff, generierte Typen

Der Manifest-Endpunkt filtert Sammlungen und Funktionen basierend auf der Rolle des anfragenden Benutzers.

Der Inhaltseditor generiert Formulare dynamisch basierend auf Felddefinitionen:

// Simplified editor rendering
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>
);
}

Jeder Feldtyp hat ein entsprechendes Widget:

FeldtypWidget
stringTexteingabe
textTextbereich
numberZahleneingabe
booleanUmschalter
datetimeDatum-/Zeitauswahl
selectDropdown
multiSelectMehrfachauswahl
portableTextTipTap-Editor
imageMedienauswahl
referenceEintragsauswahl

Portable-Text-Felder verwenden TipTap (ProseMirror) für die Bearbeitung:

User types → TipTap (ProseMirror JSON) → Save → Portable Text (DB)
Load → Portable Text (DB) → TipTap (ProseMirror JSON) → Display

Die Konvertierung erfolgt an den Lade-/Speichergrenzen über portableTextToProsemirror() und prosemirrorToPortableText().

Unterstützte Blöcke:

  • Absätze, Überschriften (H1-H6)
  • Aufzählungs- und nummerierte Listen
  • Blockzitate, Codeblöcke
  • Bilder (aus der Mediathek)
  • Links

Unbekannte Blöcke von Plugins oder Imports werden als schreibgeschützte Platzhalter erhalten.

Die Mediathek bietet:

  • Raster- und Listenansicht
  • Suche und Filter nach Typ, Datum
  • Drag-and-Drop-Upload
  • Bildvorschau mit Metadaten
  • Massenauswahl und -löschung

Uploads verwenden signierte URLs für direkten Client-zu-Speicher-Upload:

  1. Upload-URL anfordern — POST /api/media/upload-url 2. Direkt hochladen — Client lädt Datei per PUT auf signierte URL (R2/S3) 3. Upload bestätigen — POST /api/media/:id/confirm 4. Server extrahiert Metadaten — Abmessungen, MIME-Typ, etc.

Dieser Ansatz umgeht die Body-Größenbeschränkungen von Workers und ermöglicht echten Upload-Fortschritt.