Ir al contenido

Panel de Administración

El panel de administración de EmDash es una aplicación de una sola página (SPA) en React incrustada en tu sitio Astro. Proporciona una interfaz completa de gestión de contenidos para editores y administradores.

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

El administrador es una aplicación React de “gran isla”. Astro maneja la estructura base y la autenticación; toda la navegación y renderizado dentro del administrador se realiza en el lado del cliente.

CapaTecnologíaPropósito
EnrutamientoTanStack RouterEnrutamiento del lado del cliente con seguridad de tipos
DatosTanStack QueryEstado del servidor, caché, mutaciones
Interfaz de UsuarioKumoComponentes accesibles (Base UI + Tailwind)
TablasTanStack TableOrdenación, filtrado, paginación
FormulariosReact Hook Form + ZodValidación que coincide con el esquema del servidor
IconosPhosphorIconografía consistente
EditorTipTapEdición de texto enriquecido (Portable Text)

El administrador se monta en /_emdash/admin/ y utiliza enrutamiento del lado del cliente:

RutaPantalla
/Panel de control
/content/:collectionLista de contenidos
/content/:collection/:idEditor de contenido
/content/:collection/newNueva entrada
/mediaBiblioteca multimedia
/content-typesConstructor de esquemas (solo administradores)
/menusMenús de navegación
/widgetsÁreas de widgets
/taxonomiesGestión de categorías/etiquetas
/settingsConfiguración del sitio
/plugins/:pluginId/*Páginas de plugins

El administrador no codifica de forma rígida el conocimiento de colecciones o 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": "Registro de auditoria",
"adminPages": [{ "path": "history", "label": "Historial de auditoria" }],
"widgets": [{ "id": "recent-activity", "title": "Actividad reciente" }]
}
],
"taxonomies": [{ "name": "category", "label": "Categorias", "hierarchical": true }],
"version": "abc123"
}

El administrador construye su navegación, formularios y editores completamente a partir de este manifiesto. Beneficios:

  • Los cambios en el esquema aparecen inmediatamente — No se necesita reconstruir el administrador
  • La interfaz de usuario del plugin se integra automáticamente — Páginas y widgets del manifiesto
  • Seguridad de tipos en el límite — Los esquemas Zod permanecen en el servidor
  1. Carga la SPA del administrador — TanStack Router se inicializa 2. Obtener manifiesto — TanStack Query almacena en caché metadatos de colecciones/plugins 3. Construir navegación — Barra lateral generada desde el manifiesto 4. Usuario navega — Enrutamiento del lado del cliente, sin recarga de página 5. Obtener datos — TanStack Query solicita contenido de las APIs REST 6. Renderizar formularios — Editores de campos generados desde los descriptores de campo del manifiesto 7. Enviar cambios — Mutaciones vía TanStack Query, actualizaciones optimistas 8. El servidor valida — Esquemas Zod en el servidor, los errores se devuelven como JSON

El administrador se comunica exclusivamente a través de APIs REST:

MétodoEndpointPropósito
GET/api/content/:collectionListar entradas
POST/api/content/:collectionCrear entrada
GET/api/content/:collection/:idObtener entrada
PUT/api/content/:collection/:idActualizar entrada
DELETE/api/content/:collection/:idEliminar entrada (soft delete)
GET/api/content/:collection/:id/revisionsListar revisiones
POST/api/content/:collection/:id/preview-urlGenerar URL de vista previa
MétodoEndpointPropósito
GET/api/schemaExportar esquema completo
GET/api/schema/collectionsListar colecciones
POST/api/schema/collectionsCrear colección
PUT/api/schema/collections/:slugActualizar colección
DELETE/api/schema/collections/:slugEliminar colección
POST/api/schema/collections/:slug/fieldsAñadir campo
PUT/api/schema/collections/:slug/fields/:fieldActualizar campo
DELETE/api/schema/collections/:slug/fields/:fieldEliminar campo
MétodoEndpointPropósito
GET/api/mediaListar elementos multimedia
POST/api/media/upload-urlObtener URL de subida firmada
POST/api/media/:id/confirmConfirmar subida completada
DELETE/api/media/:idEliminar elemento multimedia
GET/api/media/file/:keyServir archivo multimedia
EndpointPropósito
/api/settingsConfiguración 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 de los plugins

Todos los endpoints de lista utilizan paginación basada en cursor:

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

Obtener la siguiente página:

GET /api/content/posts?cursor=eyJpZCI6IjAxSjEyMzQ1NiJ9

Los plugins pueden extender el administrador con páginas y widgets del panel de administración. La integración genera un módulo virtual con importaciones estáticas:

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

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

Los plugins pueden agregar widgets al panel de administración:

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

La ruta del shell del administrador aplica autenticación mediante middleware de 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();
}

El SPA del administrador en sí no maneja el inicio de sesión; esa es una página de Astro que establece una cookie de sesión.

Diferentes roles ven diferentes partes del administrador:

RolSecciones Visibles
EditorPanel de control, colecciones asignadas, medios
Administrador+ Tipos de contenido, todas las colecciones, configuraciones
Desarrollador+ Acceso CLI, tipos generados

El endpoint del manifiesto filtra colecciones y características según el rol del usuario solicitante.

El editor de contenido genera formularios dinámicamente basándose en definiciones de campo:

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

Cada tipo de campo tiene un widget correspondiente:

Tipo de CampoWidget
stringEntrada de texto
textÁrea de texto
numberEntrada numérica
booleanInterruptor
datetimeSelector de fecha/hora
selectLista desplegable
multiSelectSelección múltiple
portableTextEditor TipTap
imageSelector de medios
referenceSelector de entrada

Los campos de Texto Portátil utilizan TipTap (ProseMirror) para la edición:

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

La conversión ocurre en los límites de carga/guardado mediante portableTextToProsemirror() y prosemirrorToPortableText().

Bloques admitidos:

  • Párrafos, encabezados (H1-H6)
  • Listas con viñetas y numeradas
  • Citas en bloque, bloques de código
  • Imágenes (desde la biblioteca multimedia)
  • Enlaces

Los bloques desconocidos de plugins o importaciones se conservan como marcadores de posición de solo lectura.

La biblioteca multimedia proporciona:

  • Vistas de cuadrícula y lista
  • Búsqueda y filtrado por tipo, fecha
  • Carga por arrastrar y soltar
  • Vista previa de imagen con metadatos
  • Selección masiva y eliminación

Las cargas utilizan URLs firmadas para carga directa de cliente a almacenamiento:

  1. Solicitar URL de carga — POST /api/media/upload-url 2. Cargar directamente — El cliente envía el archivo mediante PUT a la URL firmada (R2/S3) 3. Confirmar carga — POST /api/media/:id/confirm 4. El servidor extrae metadatos — Dimensiones, tipo MIME, etc.

Este enfoque evita los límites de tamaño del cuerpo de Workers y proporciona un progreso de carga real.