Panneau d’administration

Sur cette page

Le panneau d’administration EmDash est une application monopage React intégrée à votre site Astro. Il fournit une interface complète de gestion de contenu pour les éditeur·rice·s et administrateur·rice·s.

Vue d’ensemble de l’architecture

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

L’admin est une grande « île » React. Astro gère le shell et l’authentification ; toute la navigation et le rendu dans l’admin sont côté client.

Pile technique

CoucheTechnologieRôle
RoutageTanStack RouterRoutage client typé
DonnéesTanStack QueryÉtat serveur, cache, mutations
UIKumoComposants accessibles (Base UI + Tailwind)
TableauxTanStack TableTri, filtrage, pagination
FormulairesReact Hook Form + ZodValidation alignée sur le schéma serveur
IcônesPhosphorIconographie cohérente
ÉditeurTipTapÉdition de texte enrichi (Portable Text)

Structure des routes

L’admin est monté sur /_emdash/admin/ et utilise le routage client :

CheminÉcran
/Tableau de bord
/content/:collectionListe de contenu
/content/:collection/:idÉditeur de contenu
/content/:collection/newNouvelle entrée
/mediaMédiathèque
/content-typesConstructeur de schéma (admin uniquement)
/menusMenus de navigation
/widgetsZones de widgets
/taxonomiesGestion catégories/étiquettes
/settingsParamètres du site
/plugins/:pluginId/*Pages de plugins

UI pilotée par manifeste

L’admin n’encode pas en dur les collections ni les plugins. Il récupère un manifeste depuis le serveur :

GET /_emdash/api/manifest

Réponse :

{
	"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"
}

L’admin construit navigation, formulaires et éditeurs entièrement à partir de ce manifeste. Avantages :

  • Les changements de schéma apparaissent tout de suite — Pas de rebuild de l’admin
  • L’UI des plugins s’intègre automatiquement — Pages et widgets depuis le manifeste
  • Sûreté de type à la frontière — Les schémas Zod restent côté serveur

Flux de données

  1. Chargement de la SPA admin — TanStack Router s’initialise 2. Récupération du manifeste — TanStack Query met en cache les métadonnées collections/plugins 3. Construction de la navigation — Barre latérale générée depuis le manifeste 4. Navigation utilisateur — Routage client, pas de rechargement de page 5. Chargement des données — TanStack Query interroge les API REST 6. Rendu des formulaires — Éditeurs de champs générés depuis les descripteurs du manifeste 7. Envoi des modifications — Mutations via TanStack Query, mises à jour optimistes 8. Validation serveur — Schémas Zod côté serveur, erreurs en JSON

Points de terminaison REST

L’admin communique exclusivement via des API REST :

API de contenu

MéthodePoint de terminaisonRôle
GET/api/content/:collectionLister les entrées
POST/api/content/:collectionCréer une entrée
GET/api/content/:collection/:idObtenir une entrée
PUT/api/content/:collection/:idMettre à jour une entrée
DELETE/api/content/:collection/:idSuppression logique
GET/api/content/:collection/:id/revisionsLister les révisions
POST/api/content/:collection/:id/preview-urlGénérer une URL d’aperçu

API de schéma

MéthodePoint de terminaisonRôle
GET/api/schemaExporter le schéma complet
GET/api/schema/collectionsLister les collections
POST/api/schema/collectionsCréer une collection
PUT/api/schema/collections/:slugMettre à jour une collection
DELETE/api/schema/collections/:slugSupprimer une collection
POST/api/schema/collections/:slug/fieldsAjouter un champ
PUT/api/schema/collections/:slug/fields/:fieldMettre à jour un champ
DELETE/api/schema/collections/:slug/fields/:fieldSupprimer un champ

API médias

MéthodePoint de terminaisonRôle
GET/api/mediaLister les médias
POST/api/media/upload-urlObtenir une URL de téléversement signée
POST/api/media/:id/confirmConfirmer la fin du téléversement
DELETE/api/media/:idSupprimer un média
GET/api/media/file/:keyServir un fichier média

Autres API

Point de terminaisonRôle
/api/settingsParamètres du site (GET/POST)
/api/menus/*Menus de navigation
/api/widget-areas/*Gestion des widgets
/api/taxonomies/*Termes de taxonomie
/api/admin/plugins/*État des plugins

Pagination

Tous les endpoints de liste utilisent une pagination par curseur :

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

Page suivante :

GET /api/content/posts?cursor=eyJpZCI6IjAxSjEyMzQ1NiJ9

UI admin des plugins

Les plugins peuvent étendre l’admin avec des pages et des widgets de tableau de bord. L’intégration génère un module virtuel avec des imports statiques :

// virtual:emdash/plugin-admins (généré)
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,
};

Pages de plugin

Les pages se montent sous /_emdash/admin/plugins/:pluginId/* :

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

Rendu à : /_emdash/admin/plugins/seo/settings

Widgets du tableau de bord

Les plugins peuvent ajouter des widgets au tableau de bord :

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

Authentification

La route shell de l’admin applique l’authentification via le middleware Astro :

// Logique middleware simplifiée
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();
}

La SPA admin ne gère pas la connexion : c’est une page Astro qui définit le cookie de session.

Accès par rôle

Chaque rôle voit des parties différentes de l’admin :

RôleSections visibles
EditorTableau de bord, collections assignées, médias
Admin+ Types de contenu, toutes les collections, paramètres
Developer+ Accès CLI, types générés

Le point de terminaison du manifeste filtre collections et fonctionnalités selon le rôle.

Éditeur de contenu

L’éditeur génère des formulaires dynamiquement à partir des définitions de champs :

// Rendu simplifié de l’éditeur
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>
	);
}

Chaque type de champ a un widget associé :

Type de champWidget
stringChamp texte
textZone de texte
numberChamp numérique
booleanInterrupteur
datetimeSélecteur date/heure
selectListe déroulante
multiSelectSélection multiple
portableTextÉditeur TipTap
imageSélecteur de médias
referenceSélecteur d’entrées

Éditeur de texte enrichi

Les champs Portable Text utilisent TipTap (ProseMirror) :

Saisie → TipTap (JSON ProseMirror) → Enregistrement → Portable Text (BD)
Chargement → Portable Text (BD) → TipTap (JSON ProseMirror) → Affichage

La conversion se fait aux frontières chargement/enregistrement via portableTextToProsemirror() et prosemirrorToPortableText().

Blocs pris en charge :

  • Paragraphes, titres (H1–H6)
  • Listes à puces et numérotées
  • Citations, blocs de code
  • Images (depuis la médiathèque)
  • Liens

Les blocs inconnus issus de plugins ou d’importations sont conservés comme espaces réservés en lecture seule.

Médiathèque

La médiathèque offre :

  • Vues grille et liste
  • Recherche et filtre par type, date
  • Glisser-déposer pour téléverser
  • Aperçu image avec métadonnées
  • Sélection et suppression par lot

Les téléversements utilisent des URL signées (client → stockage direct) :

  1. Demander l’URL de téléversementPOST /api/media/upload-url 2. Téléverser directement — PUT du fichier vers l’URL signée (R2/S3) 3. ConfirmerPOST /api/media/:id/confirm 4. Extraction des métadonnées — Dimensions, type MIME, etc.

Cela contourne les limites de taille du corps sur Workers et permet une progression réelle du téléversement.

Étapes suivantes