Panel de administración
El panel vive en /admin y lo monta la integración: no hay nada que instalar, ni una página que
escribir, ni un componente que importar. Declaras tus colecciones y ahí está.
Hoy funciona el CMS entero: el asistente de configuración inicial, el login, el menú construido desde tu config, el tema heredado de tu sitio, el listado de cada colección, el editor de documentos, la biblioteca de medios y la pantalla de ajustes. Lo que queda fuera es la pantalla de proveedores sociales, invitar a un segundo usuario y la pantalla de cuenta.
La primera vez
Sección titulada «La primera vez»Una instalación cuya tabla users está vacía no tiene login que enseñar, así que cualquier ruta del
panel lleva al asistente.
-
Abres
/adminy acabas en/admin/setup. -
Un formulario pide el nombre del sitio, su URL pública, el remitente del correo y su dirección de respuesta —los ajustes— más tu nombre, tu email y tu contraseña.
-
Al enviarlo se guardan los ajustes, se crea tu cuenta ya verificada y entras al panel con la sesión abierta.
-
A partir de ahí
/admin/setupresponde404y/api/auth/sign-up/emailresponde403.
El orden importa y no es casual: los ajustes se validan y se escriben antes de crear la cuenta. Al revés, una URL mal tecleada dejaría un administrador creado, el registro cerrado y un asistente que ya no se puede volver a abrir.
Quién entra
Sección titulada «Quién entra»| Situación | Qué pasa |
|---|---|
| Sin usuarios en la base | Todo lleva al asistente |
| Con usuarios, sin sesión | Todo lleva a /admin/login |
| Con usuarios, sin sesión, ruta inexistente | Lleva al login también — no se revela que no existe |
Con sesión, en /admin/login |
Lleva al dashboard |
| Con sesión, ruta inexistente | 404 dentro del panel |
Con usuarios, en /admin/setup |
404. El asistente terminó su trabajo |
El tema es el tuyo
Sección titulada «El tema es el tuyo»El panel no define ni un color propio: todas sus reglas leen las variables de shadcn. Si tú las defines, se pinta con las tuyas.
Como /admin sirve un documento HTML entero, tu hoja de estilos no llega sola: se la señalas con
admin.css. Está explicado en
Integración de Astro.
Y si no la señalas, el panel trae un tema de respaldo y no hay nada que configurar.
Modo oscuro
Sección titulada «Modo oscuro»El panel trae su propio conmutador —está en el menú de tu cuenta, abajo del todo— y guarda la
elección en localStorage. La clase .dark que ponga el conmutador de tu sitio no le llega: /admin
sirve otro documento.
Lo que sí comparten es el localStorage del origen. Si tu conmutador escribe la misma clave, el panel
sigue al sitio; el paquete la exporta como THEME_KEY para que no tengas que copiar la cadena. Cómo
hacerlo, en Integración de Astro.
Un script en línea en el <head> aplica la elección antes del primer pintado, así que no hay
destello blanco al recargar.
El menú sale del config
Sección titulada «El menú sale del config»La barra lateral se construye leyendo tus colecciones, sin que tengas que declarar nada:
{ slug: 'posts', labels: { singular: 'Entrada', plural: 'Entradas' }, admin: { group: 'Catálogo', hidden: false }, fields: [/* … */],}| Opción | Efecto en el menú |
|---|---|
labels.plural |
La etiqueta que se ve |
admin.group |
Agrupa la colección bajo ese epígrafe |
admin.hidden |
La quita del menú. Su URL sigue funcionando |
upload |
La quita también: es la biblioteca, y esa ya está abajo |
No se inventa ningún grupo. Lo que no declara admin.group va suelto y siempre arriba, antes
de cualquier epígrafe, esté donde esté en tu config. Cada grupo sí es un epígrafe que pliega lo que
tiene debajo, y los grupos conservan el orden de tu config, no el alfabético: ese orden es una
decisión que alguien tomó.
La única colección que el CMS clasifica por su cuenta es users, que nace bajo Autenticación: un
usuario no es contenido y no debería sentarse arriba entre las colecciones que escribes tú.
Al pasar el ratón por una colección aparece un menú con dos atajos: verla entera y crear una nueva.
Así, un config con posts sueltas y una colección de media acaba en esto:
Posts ← suelta, arribaAutenticación Users─────Escritorio · Medios · AjustesAbajo del todo, separadas de las colecciones porque no lo son, van el escritorio, los medios y los ajustes. Y al pie, tu cuenta.
La barra se pliega a iconos y recuerda cómo la dejaste en la cookie sidebar_state, así que el
servidor ya la dibuja plegada en la siguiente página.
El listado de una colección
Sección titulada «El listado de una colección»Las columnas salen de admin.defaultColumns, o de los tres primeros campos si no lo declaras. Si
nombras un campo que ya no existe, esa columna simplemente no se pinta: una errata en el config no
tumba la página.
Pulsar una cabecera ordena. El buscador traduce lo que escribes a contains sobre el campo que
declares en admin.useAsTitle — y si ese campo no es de texto no se pinta el buscador, porque
contains solo es legal sobre text, textarea y select. La paginación deja elegir entre 10, 25,
50 y 100 por página.
Y puedes filtrar por campo y operador. Cada tipo ofrece los operadores que tienen sentido para él
—contains en un texto, greater_than en un número o una fecha, in en un select— y nunca uno que
la base rechazaría. El valor lo escribes con el mismo componente que usa el editor: filtrar por autor
te da el mismo buscador de relación, no una caja donde teclear un id.
El filtro va en la URL con la misma sintaxis que la API REST, así que un filtro del panel se pega tal cual en una llamada a la API:
/admin/collections/posts?where[status][equals]=draft&where[title][contains]=astroUn filtro imposible —un campo que no existe, un operador que no aplica a ese tipo, una lista de más de cien valores— se descarta en silencio y el resto se aplica. Nunca es un error: una consulta que revienta en el frontmatter de Astro sería un 500 contando por qué, y eso describe tu esquema a quien no debería verlo.
Todo eso vive en la URL, no en el componente. Un listado ordenado por fecha, buscando «astro» y
filtrado por borradores es un enlace que puedes pegarle a alguien o guardar en marcadores, y sobrevive
a recargar. También significa que ordenar, buscar, filtrar y paginar funcionan con JavaScript
desactivado: son anclas y un formulario GET, no manejadores de eventos.
Un listado vacío dice tres cosas distintas según por qué está vacío —no hay documentos todavía, la búsqueda no encuentra nada, o has pedido una página que no existe— y cada una ofrece la acción que corresponde. Son tres situaciones diferentes y mezclarlas es mentirle a quien las lee.
Para borrar en lote marcas las casillas y confirmas en una pantalla que dice el número exacto y enumera los títulos. Si alguno falla, te lo dice: una tanda a medias nunca se reporta como éxito.
El editor
Sección titulada «El editor»Creación y edición son la misma pantalla; crear es abrirla con un documento vacío, ya relleno con los
defaultValue que declara tu config.
Los campos se reparten en dos columnas según admin.position: los de 'sidebar' a la derecha, junto
a createdAt, updatedAt y las acciones; el resto a la izquierda. admin.description se pinta como
pista bajo el campo, admin.readOnly lo deshabilita y admin.width: 'half' lo pone a media anchura.
Guardar está deshabilitado mientras no cambies nada, ⌘S / Ctrl+S guarda, y salir con cambios sin
guardar te avisa. Un error de validación del servidor se pinta bajo su campo, con el foco puesto en el
primero que falla y sin perder nada de lo que hubieras escrito.
Borrar exige escribir el título exacto del documento. La comparación la hace el servidor: la del navegador se salta abriendo las herramientas de desarrollo.
Un componente por tipo de campo
Sección titulada «Un componente por tipo de campo»Cada uno de los nueve tipos trae su propio control, y cinco hacen algo más que un input:
| Tipo | Qué te da |
|---|---|
relationship |
Un buscador que consulta la colección destino por su useAsTitle, con 300 ms de espera entre teclas y las diez primeras coincidencias. Guarda el id, te enseña el título, y abre el documento relacionado en otra pestaña |
date |
Un calendario, con hora si el campo es datetime. Lo ves y lo eliges en tu huso, rotulado; lo que se guarda sigue siendo UTC |
select |
Un <select> normal hasta nueve opciones; a partir de diez, un buscador |
json |
Un área monoespaciada que valida al salir del campo y te dice la línea y la columna del error. Un JSON roto no se propaga. Y un botón para reindentar |
upload |
Miniatura, y un diálogo con tu biblioteca de medios filtrada por el accept del campo. También puedes arrastrar un fichero encima, con una condición: ver abajo |
Si la colección a la que apunta una relación está vacía, en vez de una lista vacía te ofrece crear el
primer documento. Y si su useAsTitle no es de texto, el buscador no aparece —contains solo es
legal sobre text, textarea y select— y queda el campo de id.
Poner el tuyo
Sección titulada «Poner el tuyo»Puedes sustituir el componente de un tipo por uno tuyo, desde cms.config.ts:
export default defineConfig({ admin: { fieldComponents: { json: './src/cms/JsonEditor.tsx#JsonEditor', }, }, collections: [posts],})Es una ruta, no una referencia. La ruta es relativa a la raíz de tu proyecto de Astro, igual que
admin.css, y lleva opcionalmente un #NombreDeLaExportación; sin él se usa la exportación por
defecto. Si la ruta no existe, Astro no arranca y el error te dice cuál era y de qué campo.
Tu componente recibe lo mismo que el de serie —el campo, el valor, un onChange, el error y si está
deshabilitado— y sustituye al original en todas partes: el editor, el panel de detalle de medios,
el formulario de subida y la barra de filtros. Es la vía para meter un editor de código de verdad, un
selector de color o un mapa sin tocar el paquete.
La biblioteca de medios
Sección titulada «La biblioteca de medios»Una cuadrícula con las miniaturas de tus colecciones upload. Al seleccionar un fichero se abre su
panel de detalle en una URL propia (?file=<id>), no en un diálogo, así que el botón atrás
funciona y el enlace se puede compartir. Dentro están sus dimensiones, su tamaño, su URL pública
seleccionable y sus campos, editables.
Se suben varios ficheros a la vez, arrastrándolos o con el selector de siempre. Si uno falla —por
ejemplo un fichero de texto renombrado a .png, que el olfateo de bytes rechaza— se te dice cuál por
su nombre, y los demás de la misma tanda sí entran.
La pantalla de ajustes
Sección titulada «La pantalla de ajustes»/admin/settings edita los seis ajustes del sitio que viven en KV. Al guardar
te avisa de que los cambios tardan hasta un minuto en propagarse, que es la ventana de KV más la caché
del CMS, no una estimación por lo bajo.
Los proveedores sociales todavía no tienen pantalla, ni lado servidor.
El logo y el favicon
Sección titulada «El logo y el favicon»El panel trae una marca propia y la usa en tres sitios: la baldosa de la cabecera del menú, la
tarjeta del login y del asistente, y el <link rel="icon"> de la página.
En pantalla el dibujo va en currentColor, así que hereda el color de la baldosa —que es el
--sidebar-primary de tu tema— en vez de traer uno fijo que se pelee con él.
Para cambiar el icono de la pestaña, el ajuste favicon:
await Astro.locals.cms.settings.update({ favicon: '/mi-logo.svg' })Vale una ruta de tu sitio o una URL completa. Mientras esté vacío se usa el que trae Kevin CMS, que
viaja dentro del propio JavaScript del panel como un data: URI — el paquete no sirve ficheros
estáticos, así que no hay una URL que pedirle.
Tu cuenta
Sección titulada «Tu cuenta»La tarjeta del pie muestra tu nombre, tu correo y tu avatar —o tus iniciales, si no tienes uno—, y abre un menú con tres cosas: la pantalla de cuenta, el conmutador de tema y cerrar sesión.
Cerrar sesión es un formulario de verdad, no una llamada en segundo plano: pasa por el mismo camino que cualquier otra escritura del panel, borra la sesión de la base —no solo la cookie de tu navegador— y te deja en la pantalla de entrada.
Funciona sin JavaScript
Sección titulada «Funciona sin JavaScript»Los formularios del panel son <form method="POST"> de toda la vida, enviados a su propia URL. React
los mejora —validación inmediata, errores sin recargar— pero no es lo que los hace funcionar: con
JavaScript desactivado, el login y el asistente siguen entrando.
Cuando el servidor rechaza un envío responde 400 y vuelve a dibujar el formulario con el error sobre
el campo que lo causó y lo que habías tecleado intacto. La contraseña no: un campo de contraseña
devuelto dentro del HTML acaba en el registro de cada proxy por el que pasa.
Por qué se renderiza en el servidor
Sección titulada «Por qué se renderiza en el servidor»El panel es una página de Astro que lee Astro.locals.cms en su frontmatter, con React solo en las
piezas que necesitan comportamiento. No es una aplicación de una sola página que hable por HTTP con tu
propio servidor.
La razón es que la API local llega ya tipada por lo que genera el codegen, y la API REST no tiene tipos de cliente generados: una SPA tendría que mantener a mano una copia de cada interfaz que ya existe. Y el salto de red no compra nada en una página que el servidor está renderizando de todos modos.
El precio es el aviso de más arriba: por la API local no pasa ningún control de acceso, así que la guarda tiene que ser impecable.
Las dos veces que el navegador sí pide algo
Sección titulada «Las dos veces que el navegador sí pide algo»El panel se diseñó para no hablar por HTTP con tu propio servidor, y sigue siendo así en todas las pantallas menos en dos puntos del editor:
- buscar en el campo
relationship, porque una colección de diez mil filas no cabe en un desplegable y sembrarla desde el servidor no escala; - elegir o arrastrar un fichero en el campo
upload.
Resolverlo navegando —un panel de selección con su propia URL, como el ?file= de Medios— habría
perdido lo que aún no has guardado en el formulario. Esa es la razón entera.
Las peticiones van a tu propia API REST, al mismo origen, así que llevan tu cookie de sesión y CORS nunca entra en juego. Están confinadas a un solo módulo del paquete para que la excepción se pueda auditar de un vistazo, y no hay ninguna otra: el resto del panel sigue sin pedir nada.
Lo que todavía no hay
Sección titulada «Lo que todavía no hay»| Cosa | Estado |
|---|---|
| Proveedores sociales en Ajustes | ❌ No hay ni pantalla ni lado servidor |
| Invitar a un segundo usuario | ❌ El registro queda cerrado tras el primero |
| Pantalla de cuenta | ❌ Enrutada en /admin/account, sin dibujar |
| Barra de progreso al subir | ❌ Exigiría pedir desde el navegador, y el panel no lo hace |
| Roles y permisos | ❌ Toda cuenta con sesión hace de todo |
| Traducir el panel | ❌ Está en español |