El panel de administración de EmDash es una aplicación de una sola página (SPA) en React incrustada en tu sitio Astro. Ofrece una interfaz completa de gestión de contenido para editoras y administradoras.
Descripción de la arquitectura
┌────────────────────────────────────────────────────────────────┐
│ Shell de Astro │
│ /_emdash/admin/[...path].astro │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ SPA de React │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │
│ │ │ TanStack │ │ TanStack │ │ Kumo │ │ │
│ │ │ Router │ │ Query │ │ Componentes │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ Cliente API REST │ │ │
│ │ │ /_emdash/api/* │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
El admin es una «gran isla» de React. Astro gestiona el shell y la autenticación; toda la navegación y el renderizado dentro del admin son del lado del cliente.
Pila tecnológica
| Capa | Tecnología | Propósito |
|---|---|---|
| Enrutamiento | TanStack Router | Enrutamiento del cliente con tipos seguros |
| Datos | TanStack Query | Estado del servidor, caché, mutaciones |
| UI | Kumo | Componentes accesibles (Base UI + Tailwind) |
| Tablas | TanStack Table | Ordenación, filtrado, paginación |
| Formularios | React Hook Form + Zod | Validación alineada con el esquema del servidor |
| Iconos | Phosphor | Iconografía coherente |
| Editor | TipTap | Edición de texto enriquecido (Portable Text) |
Estructura de rutas
El admin se monta en /_emdash/admin/ y usa enrutamiento del lado del cliente:
| Ruta | Pantalla |
|---|---|
/ | Panel principal |
/content/:collection | Lista de contenido |
/content/:collection/:id | Editor de contenido |
/content/:collection/new | Nueva entrada |
/media | Biblioteca multimedia |
/content-types | Constructor de esquema (solo admin) |
/menus | Menús de navegación |
/widgets | Áreas de widgets |
/taxonomies | Gestión de categorías/etiquetas |
/settings | Ajustes del sitio |
/plugins/:pluginId/* | Páginas de plugins |
UI basada en manifiesto
El admin no codifica a mano las colecciones ni los plugins. En su lugar, obtiene un manifiesto del servidor:
GET /_emdash/api/manifest
Respuesta:
{
"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"
}
El admin construye navegación, formularios y editores solo a partir de este manifiesto. Ventajas:
- Los cambios de esquema se ven al instante — No hace falta reconstruir el admin
- La UI de plugins se integra sola — Páginas y widgets desde el manifiesto
- Tipos en el límite — Los esquemas Zod permanecen en el servidor
Flujo de datos
- Carga la SPA del admin — TanStack Router se inicializa 2. Obtener manifiesto — TanStack Query en caché los metadatos de colecciones/plugins 3. Construir navegación — Barra lateral generada desde el manifiesto 4. La usuaria navega — Enrutamiento del cliente, sin recargar página 5. Obtener datos — TanStack Query pide contenido a las API REST 6. Renderizar formularios — Editores de campo generados desde los descriptores del manifiesto 7. Enviar cambios — Mutaciones vía TanStack Query, actualizaciones optimistas 8. El servidor valida — Esquemas Zod en el servidor, errores en JSON
Puntos finales REST
El admin se comunica solo mediante API REST:
API de contenido
| Método | Endpoint | Propósito |
|---|---|---|
GET | /api/content/:collection | Listar entradas |
POST | /api/content/:collection | Crear entrada |
GET | /api/content/:collection/:id | Obtener entrada |
PUT | /api/content/:collection/:id | Actualizar entrada |
DELETE | /api/content/:collection/:id | Borrado lógico de entrada |
GET | /api/content/:collection/:id/revisions | Listar revisiones |
POST | /api/content/:collection/:id/preview-url | Generar URL de vista previa |
API de esquema
| Método | Endpoint | Propósito |
|---|---|---|
GET | /api/schema | Exportar esquema completo |
GET | /api/schema/collections | Listar colecciones |
POST | /api/schema/collections | Crear colección |
PUT | /api/schema/collections/:slug | Actualizar colección |
DELETE | /api/schema/collections/:slug | Eliminar colección |
POST | /api/schema/collections/:slug/fields | Añadir campo |
PUT | /api/schema/collections/:slug/fields/:field | Actualizar campo |
DELETE | /api/schema/collections/:slug/fields/:field | Eliminar campo |
API de medios
| Método | Endpoint | Propósito |
|---|---|---|
GET | /api/media | Listar elementos multimedia |
POST | /api/media/upload-url | Obtener URL de subida firmada |
POST | /api/media/:id/confirm | Confirmar subida completada |
DELETE | /api/media/:id | Eliminar elemento multimedia |
GET | /api/media/file/:key | Servir archivo multimedia |
Otras API
| Endpoint | Propósito |
|---|---|
/api/settings | Ajustes del sitio (GET/POST) |
/api/menus/* | Menús de navegación |
/api/widget-areas/* | Gestión de widgets |
/api/taxonomies/* | Términos de taxonomía |
/api/admin/plugins/* | Estado del plugin |
Paginación
Todos los endpoints de lista usan paginación por cursor:
{
"items": [...],
"nextCursor": "eyJpZCI6IjAxSjEyMzQ1NiJ9"
}
Obtener la página siguiente:
GET /api/content/posts?cursor=eyJpZCI6IjAxSjEyMzQ1NiJ9
UI de admin de plugins
Los plugins pueden ampliar el admin con páginas y widgets del panel. La integración genera un módulo virtual con imports estáticos:
// virtual:emdash/plugin-admins (generado)
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,
};
Páginas de plugin
Las páginas de plugin se montan bajo /_emdash/admin/plugins/:pluginId/*:
// @emdash-cms/plugin-seo/src/admin.tsx
export const pages = [
{
path: "settings",
component: SEOSettingsPage,
label: "SEO Settings",
},
];
Se renderiza en: /_emdash/admin/plugins/seo/settings
Widgets del panel
Los plugins pueden añadir widgets al panel:
export const widgets = [
{
id: "seo-overview",
component: SEOWidget,
title: "SEO Overview",
size: "half", // "full" | "half" | "third"
},
];
Autenticación
La ruta shell del admin exige autenticación mediante middleware de Astro:
// Lógica simplificada del middleware
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 del admin no gestiona el inicio de sesión: eso es una página de Astro que establece la cookie de sesión.
Acceso por roles
Cada rol ve partes distintas del admin:
| Rol | Secciones visibles |
|---|---|
| Editor | Panel, colecciones asignadas, multimedia |
| Admin | + Tipos de contenido, todas las colecciones, ajustes |
| Developer | + Acceso CLI, tipos generados |
El endpoint del manifiesto filtra colecciones y funciones según el rol de la usuaria.
Editor de contenido
El editor de contenido genera formularios dinámicamente según las definiciones de campo:
// Renderizado simplificado del editor
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>
);
}
Cada tipo de campo tiene un widget correspondiente:
| Tipo de campo | Widget |
|---|---|
string | Entrada de texto |
text | Área de texto |
number | Entrada numérica |
boolean | Interruptor |
datetime | Selector de fecha/hora |
select | Desplegable |
multiSelect | Selección múltiple |
portableText | Editor TipTap |
image | Selector de medios |
reference | Selector de entradas |
Editor de texto enriquecido
Los campos Portable Text usan TipTap (ProseMirror) para editar:
La usuaria escribe → TipTap (JSON ProseMirror) → Guardar → Portable Text (BD)
Cargar → Portable Text (BD) → TipTap (JSON ProseMirror) → Mostrar
La conversión ocurre en los límites de carga/guardado con portableTextToProsemirror() y prosemirrorToPortableText().
Bloques admitidos:
- Párrafos, encabezados (H1–H6)
- Listas con viñetas y numeradas
- Citas, bloques de código
- Imágenes (desde la biblioteca multimedia)
- Enlaces
Los bloques desconocidos de plugins o importaciones se conservan como marcadores de solo lectura.
Biblioteca multimedia
La biblioteca multimedia ofrece:
- Vista de cuadrícula y lista
- Búsqueda y filtro por tipo y fecha
- Subida por arrastrar y soltar
- Vista previa de imagen con metadatos
- Selección y eliminación por lotes
Las subidas usan URLs firmadas para subida directa cliente → almacenamiento:
- Solicitar URL de subida —
POST /api/media/upload-url2. Subir directamente — El cliente hace PUT del archivo a la URL firmada (R2/S3) 3. Confirmar subida —POST /api/media/:id/confirm4. El servidor extrae metadatos — Dimensiones, tipo MIME, etc.
Así se evitan límites de tamaño del cuerpo en Workers y se obtiene progreso real de subida.