Admin-Panel

Auf dieser Seite

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

LayerTechnologieZweck
RoutingTanStack RouterTypsicheres clientseitiges Routing
DatenTanStack QueryServer-State, Caching, Mutationen
UIKumoBarrierefreie Komponenten (Base UI + Tailwind)
TabellenTanStack TableSortierung, Filter, Paginierung
FormulareReact Hook Form + ZodValidierung passend zum Server-Schema
IconsPhosphorEinheitliche Iconografie
EditorTipTapRich-Text (Portable Text)

Routenstruktur

Das Admin hängt unter /_emdash/admin/ und nutzt clientseitiges Routing:

PfadAnsicht
/Dashboard
/content/:collectionInhaltsliste
/content/:collection/:idInhaltseditor
/content/:collection/newNeuer Eintrag
/mediaMedienbibliothek
/content-typesSchema-Builder (nur Admin)
/menusNavigationsmenüs
/widgetsWidget-Bereiche
/taxonomiesKategorien/Schlagwörter
/settingsSite-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

  1. 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

MethodeEndpunktZweck
GET/api/content/:collectionEinträge auflisten
POST/api/content/:collectionEintrag anlegen
GET/api/content/:collection/:idEintrag laden
PUT/api/content/:collection/:idEintrag aktualisieren
DELETE/api/content/:collection/:idEintrag soft löschen
GET/api/content/:collection/:id/revisionsRevisionen auflisten
POST/api/content/:collection/:id/preview-urlVorschau-URL erzeugen

Schema-APIs

MethodeEndpunktZweck
GET/api/schemaGesamtes Schema exportieren
GET/api/schema/collectionsCollections auflisten
POST/api/schema/collectionsCollection anlegen
PUT/api/schema/collections/:slugCollection aktualisieren
DELETE/api/schema/collections/:slugCollection 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

Medien-APIs

MethodeEndpunktZweck
GET/api/mediaMedien auflisten
POST/api/media/upload-urlSignierte Upload-URL
POST/api/media/:id/confirmUpload bestätigen
DELETE/api/media/:idMedium löschen
GET/api/media/file/:keyDatei ausliefern

Weitere APIs

EndpunktZweck
/api/settingsSite-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:

RolleSichtbare Bereiche
EditorDashboard, 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:

FeldtypWidget
stringTextfeld
textTextarea
numberZahlenfeld
booleanToggle
datetimeDatum/Zeit-Auswahl
selectDropdown
multiSelectMehrfachauswahl
portableTextTipTap-Editor
imageMedien-Auswahl
referenceEintrags-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:

  1. Upload-URL anfordernPOST /api/media/upload-url 2. Direkt hochladen — Client PUT auf signierte URL (R2/S3) 3. Upload bestätigenPOST /api/media/:id/confirm 4. Server extrahiert Metadaten — Abmessungen, MIME-Typ, usw.

So umgehst du Worker-Body-Größenlimits und erhältst echten Upload-Fortschritt.

Nächste Schritte