Aller au contenu

Panneau d'administration

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

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

L’administration est une application React “grande île”. Astro gère l’enveloppe et l’authentification ; toute la navigation et le rendu à l’intérieur de l’administration se font côté client.

CoucheTechnologieObjectif
RoutageTanStack RouterRoutage côté client typé
DonnéesTanStack QueryÉtat serveur, mise en cache, mutations
InterfaceKumoComposants accessibles (Base UI + Tailwind)
TableauxTanStack TableTri, filtrage, pagination
FormulairesReact Hook Form + ZodValidation correspondant au schéma serveur
IcônesPhosphorIconographie cohérente
ÉditeurTipTapÉdition de texte enrichi (Portable Text)

L’administration est montée sur /_emdash/admin/ et utilise le routage côté client :

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

L’administration ne code pas en dur la connaissance des collections ou des extensions. Au lieu de cela, elle 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": "Journal d'audit",
"adminPages": [{ "path": "history", "label": "Historique d'audit" }],
"widgets": [{ "id": "recent-activity", "title": "Activité récente" }]
}
],
"taxonomies": [{ "name": "category", "label": "Catégories", "hierarchical": true }],
"version": "abc123"
}

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

  • Les modifications de schéma apparaissent immédiatement — Aucune reconstruction de l’administration nécessaire
  • L’interface des extensions s’intègre automatiquement — Pages et widgets provenant du manifeste
  • Sécurité des types à la frontière — Les schémas Zod restent sur le serveur
  1. Chargement de l’application monopage d’administration — TanStack Router s’initialise 2. Récupération du manifeste — TanStack Query met en cache les métadonnées des collections/extensions 3. Construction de la navigation — Barre latérale générée à partir du manifeste 4. Navigation de l’utilisateur — Routage côté client, pas de rechargement de page 5. Récupération des données — TanStack Query demande du contenu aux API REST 6. Rendu des formulaires — Éditeurs de champs générés à partir des descripteurs de champ du manifeste 7. Soumission des modifications — Mutations via TanStack Query, mises à jour optimistes 8. Validation serveur — Schémas Zod sur le serveur, les erreurs sont renvoyées en JSON

L’administration communique exclusivement via des API REST :

MéthodePoint de terminaisonObjectif
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/:idSupprimer une entrée (logique)
GET/api/content/:collection/:id/revisionsLister les révisions
POST/api/content/:collection/:id/preview-urlGénérer une URL de prévisualisation
MéthodePoint de terminaisonObjectif
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
MéthodePoint de terminaisonObjectif
GET/api/mediaLister les éléments multimé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 élément multimédia
GET/api/media/file/:keyServir le fichier multimédia
Point de terminaisonObjectif
/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 extensions

Tous les points de terminaison de liste utilisent une pagination basée sur un curseur :

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

Récupérer la page suivante :

GET /api/content/posts?cursor=eyJpZCI6IjAxSjEyMzQ1NiJ9

Les plugins peuvent étendre l’administration 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 (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,
};

Les pages de plugin sont montées sous /_emdash/admin/plugins/:pluginId/* :

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

S’affiche à : /_emdash/admin/plugins/seo/settings

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"
},
];

La route de l’interface d’administration applique l’authentification via un middleware Astro :

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

L’application monopage d’administration elle-même ne gère pas la connexion — c’est une page Astro qui définit un cookie de session.

Différents rôles voient différentes parties de l’administration :

RôleSections visibles
ÉditeurTableau de bord, collections assignées, médias
Administrateur+ Types de contenu, toutes les collections, paramètres
Développeur+ Accès CLI, types générés

Le point de terminaison du manifeste filtre les collections et les fonctionnalités en fonction du rôle de l’utilisateur qui fait la demande.

L’éditeur de contenu génère des formulaires dynamiquement en fonction des définitions de champs :

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

Chaque type de champ a un widget correspondant :

Type de champWidget
stringChamp texte
textZone de texte
numberChamp numérique
booleanInterrupteur
datetimeSélecteur date/heure
selectMenu déroulant
multiSelectSélection multiple
portableTextÉditeur TipTap
imageSélecteur de média
referenceSélecteur d’entrée

Les champs Portable Text utilisent TipTap (ProseMirror) pour l’édition :

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

La conversion se produit aux limites de chargement/sauvegarde 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 bibliothèque multimédia)
  • Liens

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

La bibliothèque multimédia fournit :

  • Vues en grille et en liste
  • Recherche et filtrage par type, date
  • Téléversement par glisser-déposer
  • Aperçu d’image avec métadonnées
  • Sélection et suppression en masse

Les téléversements utilisent des URL signées pour un téléversement direct du client vers le stockage :

  1. Demander une URL de téléversement — POST /api/media/upload-url 2. Téléverser directement — Le client effectue un PUT du fichier vers l’URL signée (R2/S3) 3. Confirmer le téléversement — POST /api/media/:id/confirm 4. Le serveur extrait les métadonnées — Dimensions, type MIME, etc.

Cette approche contourne les limites de taille de corps des Workers et fournit une progression de téléversement en temps réel.