Das EmDash-Admin-Panel ist eine React-Single-Page-Application, die in deine Astro-Site eingebettet ist. Es bietet eine vollständige Content-Management-Oberfläche für Redakteurinnen und Administratorinnen.
Architekturüberblick
┌────────────────────────────────────────────────────────────────┐
│ Astro Shell │
│ /_emdash/admin/[...path].astro │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ React SPA │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │
│ │ │ TanStack │ │ TanStack │ │ Kumo │ │ │
│ │ │ Router │ │ Query │ │ Components │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ REST API Client │ │ │
│ │ │ /_emdash/api/* │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
Das Admin ist eine große React-„Insel“. Astro übernimmt Shell und Authentifizierung; Navigation und Rendering innerhalb des Admins laufen clientseitig.
Technologie-Stack
| Layer | Technologie | Zweck |
|---|---|---|
| Routing | TanStack Router | Typsicheres clientseitiges Routing |
| Daten | TanStack Query | Server-State, Caching, Mutationen |
| UI | Kumo | Barrierefreie Komponenten (Base UI + Tailwind) |
| Tabellen | TanStack Table | Sortierung, Filter, Paginierung |
| Formulare | React Hook Form + Zod | Validierung passend zum Server-Schema |
| Icons | Phosphor | Einheitliche Iconografie |
| Editor | TipTap | Rich-Text (Portable Text) |
Routenstruktur
Das Admin hängt unter /_emdash/admin/ und nutzt clientseitiges Routing:
| Pfad | Ansicht |
|---|---|
/ | Dashboard |
/content/:collection | Inhaltsliste |
/content/:collection/:id | Inhaltseditor |
/content/:collection/new | Neuer Eintrag |
/media | Medienbibliothek |
/content-types | Schema-Builder (nur Admin) |
/menus | Navigationsmenüs |
/widgets | Widget-Bereiche |
/taxonomies | Kategorien/Schlagwörter |
/settings | Site-Einstellungen |
/plugins/:pluginId/* | Plugin-Seiten |
Manifest-gesteuerte UI
Das Admin hardcodiert weder Collections noch Plugins. Stattdessen lädt es ein Manifest vom Server:
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": "Audit Log",
"adminPages": [{ "path": "history", "label": "Audit History" }],
"widgets": [{ "id": "recent-activity", "title": "Recent Activity" }]
}
],
"taxonomies": [{ "name": "category", "label": "Categories", "hierarchical": true }],
"version": "abc123"
}
Navigation, Formulare und Editoren werden vollständig aus diesem Manifest aufgebaut. Vorteile:
- Schema-Änderungen sofort sichtbar — kein Admin-Neubuild nötig
- Plugin-UI integriert sich automatisch — Seiten und Widgets aus dem Manifest
- Typsicherheit an der Grenze — Zod-Schemas bleiben auf dem Server
Datenfluss
- Admin-SPA lädt — TanStack Router initialisiert 2. Manifest laden — TanStack Query cached Collection-/Plugin-Metadaten 3. Navigation bauen — Sidebar aus Manifest 4. Nutzer navigiert — clientseitiges Routing, kein Reload 5. Daten laden — TanStack Query fragt REST-APIs 6. Formulare rendern — Feld-Editoren aus Manifest-Feldbeschreibungen 7. Änderungen senden — Mutationen über TanStack Query, optimistische Updates 8. Server validiert — Zod-Schemas auf dem Server, Fehler als JSON
REST-API-Endpunkte
Das Admin spricht ausschließlich über REST-APIs:
Content-APIs
| Methode | Endpunkt | Zweck |
|---|---|---|
GET | /api/content/:collection | Einträge auflisten |
POST | /api/content/:collection | Eintrag anlegen |
GET | /api/content/:collection/:id | Eintrag laden |
PUT | /api/content/:collection/:id | Eintrag aktualisieren |
DELETE | /api/content/:collection/:id | Eintrag soft löschen |
GET | /api/content/:collection/:id/revisions | Revisionen auflisten |
POST | /api/content/:collection/:id/preview-url | Vorschau-URL erzeugen |
Schema-APIs
| Methode | Endpunkt | Zweck |
|---|---|---|
GET | /api/schema | Gesamtes Schema exportieren |
GET | /api/schema/collections | Collections auflisten |
POST | /api/schema/collections | Collection anlegen |
PUT | /api/schema/collections/:slug | Collection aktualisieren |
DELETE | /api/schema/collections/:slug | Collection löschen |
POST | /api/schema/collections/:slug/fields | Feld hinzufügen |
PUT | /api/schema/collections/:slug/fields/:field | Feld aktualisieren |
DELETE | /api/schema/collections/:slug/fields/:field | Feld löschen |
Medien-APIs
| Methode | Endpunkt | Zweck |
|---|---|---|
GET | /api/media | Medien auflisten |
POST | /api/media/upload-url | Signierte Upload-URL |
POST | /api/media/:id/confirm | Upload bestätigen |
DELETE | /api/media/:id | Medium löschen |
GET | /api/media/file/:key | Datei ausliefern |
Weitere APIs
| Endpunkt | Zweck |
|---|---|
/api/settings | Site-Einstellungen (GET/POST) |
/api/menus/* | Navigationsmenüs |
/api/widget-areas/* | Widget-Verwaltung |
/api/taxonomies/* | Taxonomie-Terme |
/api/admin/plugins/* | Plugin-Status |
Paginierung
Alle Listen-Endpunkte nutzen cursorbasierte Paginierung:
{
"items": [...],
"nextCursor": "eyJpZCI6IjAxSjEyMzQ1NiJ9"
}
Nächste Seite:
GET /api/content/posts?cursor=eyJpZCI6IjAxSjEyMzQ1NiJ9
Plugin-Admin-UI
Plugins können das Admin um Seiten und Dashboard-Widgets erweitern. Die Integration erzeugt ein virtuelles Modul mit statischen Imports:
// virtual:emdash/plugin-admins (generiert)
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
Plugin-Seiten hängen unter /_emdash/admin/plugins/:pluginId/*:
// @emdash-cms/plugin-seo/src/admin.tsx
export const pages = [
{
path: "settings",
component: SEOSettingsPage,
label: "SEO Settings",
},
];
Gerendert unter: /_emdash/admin/plugins/seo/settings
Dashboard-Widgets
Plugins können Widgets zum Dashboard hinzufügen:
export const widgets = [
{
id: "seo-overview",
component: SEOWidget,
title: "SEO Overview",
size: "half", // "full" | "half" | "third"
},
];
Authentifizierung
Die Admin-Shell-Route erzwingt Authentifizierung über Astro-Middleware:
// Vereinfachte Middleware-Logik
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 übernimmt kein Login – das erledigt eine Astro-Seite, die ein Session-Cookie setzt.
Rollenbasierte Zugriffe
Rollen sehen unterschiedliche Admin-Bereiche:
| Rolle | Sichtbare Bereiche |
|---|---|
| Editor | Dashboard, zugewiesene Collections, Medien |
| Admin | + Content Types, alle Collections, Einstellungen |
| Developer | + CLI-Zugriff, generierte Typen |
Der Manifest-Endpunkt filtert Collections und Features nach Rolle der anfragenden Person.
Inhaltseditor
Der Content-Editor erzeugt Formulare dynamisch aus Felddefinitionen:
// Vereinfachtes 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 passendes Widget:
| Feldtyp | Widget |
|---|---|
string | Textfeld |
text | Textarea |
number | Zahlenfeld |
boolean | Toggle |
datetime | Datum/Zeit-Auswahl |
select | Dropdown |
multiSelect | Mehrfachauswahl |
portableText | TipTap-Editor |
image | Medien-Auswahl |
reference | Eintrags-Auswahl |
Rich-Text-Editor
Portable-Text-Felder nutzen TipTap (ProseMirror) zum Bearbeiten:
Eingabe → TipTap (ProseMirror JSON) → Speichern → Portable Text (DB)
Laden → Portable Text (DB) → TipTap (ProseMirror JSON) → Anzeige
Konvertierung an Lade-/Speichergrenzen über portableTextToProsemirror() und prosemirrorToPortableText().
Unterstützte Blöcke:
- Absätze, Überschriften (H1–H6)
- Aufzählungs- und nummerierte Listen
- Zitate, Codeblöcke
- Bilder (aus der Medienbibliothek)
- Links
Unbekannte Blöcke aus Plugins oder Importen bleiben als schreibgeschützte Platzhalter erhalten.
Medienbibliothek
Die Medienbibliothek bietet:
- Raster- und Listenansicht
- Suche und Filter nach Typ, Datum
- Drag-and-Drop-Upload
- Bildvorschau mit Metadaten
- Mehrfachauswahl und Löschen
Uploads nutzen signierte URLs für direkten Client-zu-Speicher-Upload:
- Upload-URL anfordern —
POST /api/media/upload-url2. Direkt hochladen — Client PUT auf signierte URL (R2/S3) 3. Upload bestätigen —POST /api/media/:id/confirm4. Server extrahiert Metadaten — Abmessungen, MIME-Typ, usw.
So umgehst du Worker-Body-Größenlimits und erhältst echten Upload-Fortschritt.