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
| Couche | Technologie | Rôle |
|---|---|---|
| Routage | TanStack Router | Routage client typé |
| Données | TanStack Query | État serveur, cache, mutations |
| UI | Kumo | Composants accessibles (Base UI + Tailwind) |
| Tableaux | TanStack Table | Tri, filtrage, pagination |
| Formulaires | React Hook Form + Zod | Validation alignée sur le schéma serveur |
| Icônes | Phosphor | Iconographie cohérente |
| Éditeur | TipTap | É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/:collection | Liste de contenu |
/content/:collection/:id | Éditeur de contenu |
/content/:collection/new | Nouvelle entrée |
/media | Médiathèque |
/content-types | Constructeur de schéma (admin uniquement) |
/menus | Menus de navigation |
/widgets | Zones de widgets |
/taxonomies | Gestion catégories/étiquettes |
/settings | Paramè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
- 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éthode | Point de terminaison | Rôle |
|---|---|---|
GET | /api/content/:collection | Lister les entrées |
POST | /api/content/:collection | Créer une entrée |
GET | /api/content/:collection/:id | Obtenir une entrée |
PUT | /api/content/:collection/:id | Mettre à jour une entrée |
DELETE | /api/content/:collection/:id | Suppression logique |
GET | /api/content/:collection/:id/revisions | Lister les révisions |
POST | /api/content/:collection/:id/preview-url | Générer une URL d’aperçu |
API de schéma
| Méthode | Point de terminaison | Rôle |
|---|---|---|
GET | /api/schema | Exporter le schéma complet |
GET | /api/schema/collections | Lister les collections |
POST | /api/schema/collections | Créer une collection |
PUT | /api/schema/collections/:slug | Mettre à jour une collection |
DELETE | /api/schema/collections/:slug | Supprimer une collection |
POST | /api/schema/collections/:slug/fields | Ajouter un champ |
PUT | /api/schema/collections/:slug/fields/:field | Mettre à jour un champ |
DELETE | /api/schema/collections/:slug/fields/:field | Supprimer un champ |
API médias
| Méthode | Point de terminaison | Rôle |
|---|---|---|
GET | /api/media | Lister les médias |
POST | /api/media/upload-url | Obtenir une URL de téléversement signée |
POST | /api/media/:id/confirm | Confirmer la fin du téléversement |
DELETE | /api/media/:id | Supprimer un média |
GET | /api/media/file/:key | Servir un fichier média |
Autres API
| Point de terminaison | Rôle |
|---|---|
/api/settings | Paramè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ôle | Sections visibles |
|---|---|
| Editor | Tableau 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 champ | Widget |
|---|---|
string | Champ texte |
text | Zone de texte |
number | Champ numérique |
boolean | Interrupteur |
datetime | Sélecteur date/heure |
select | Liste déroulante |
multiSelect | Sélection multiple |
portableText | Éditeur TipTap |
image | Sélecteur de médias |
reference | Sé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) :
- Demander l’URL de téléversement —
POST /api/media/upload-url2. Téléverser directement — PUT du fichier vers l’URL signée (R2/S3) 3. Confirmer —POST /api/media/:id/confirm4. 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.