Panel de administración

En esta página

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

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

Estructura de rutas

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

RutaPantalla
/Panel principal
/content/:collectionLista de contenido
/content/:collection/:idEditor de contenido
/content/:collection/newNueva entrada
/mediaBiblioteca multimedia
/content-typesConstructor de esquema (solo admin)
/menusMenús de navegación
/widgetsÁreas de widgets
/taxonomiesGestión de categorías/etiquetas
/settingsAjustes 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

  1. 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é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/:idBorrado lógico de entrada
GET/api/content/:collection/:id/revisionsListar revisiones
POST/api/content/:collection/:id/preview-urlGenerar URL de vista previa

API de esquema

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

API de medios

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

Otras API

EndpointPropósito
/api/settingsAjustes 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:

RolSecciones visibles
EditorPanel, 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 campoWidget
stringEntrada de texto
textÁrea de texto
numberEntrada numérica
booleanInterruptor
datetimeSelector de fecha/hora
selectDesplegable
multiSelectSelección múltiple
portableTextEditor TipTap
imageSelector de medios
referenceSelector 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:

  1. Solicitar URL de subidaPOST /api/media/upload-url 2. Subir directamente — El cliente hace PUT del archivo a la URL firmada (R2/S3) 3. Confirmar subidaPOST /api/media/:id/confirm 4. 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.

Próximos pasos