Integración de Astro
La única pieza que instalas. Monta el admin, la API REST y
Astro.locals.cms a partir de tu cms.config.ts.
import cloudflare from '@astrojs/cloudflare'import react from '@astrojs/react'import cms from '@kevolution-co/cms/astro'import { defineConfig } from 'astro/config'import cmsConfig from './cms.config'
export default defineConfig({ output: 'server', adapter: cloudflare(), integrations: [react(), cms(cmsConfig)],})El config se importa y se pasa. La integración lo usa para las rutas y el codegen, y sus propias rutas
lo vuelven a importar del fichero, así que el objeto que le pases y el que lea el worker tienen que ser
el mismo: importa tu cms.config.ts y no construyas otro ahí mismo.
Opciones
Sección titulada «Opciones»function cms(config: ResolvedConfig, options?: IntegrationOptions): AstroIntegration
interface IntegrationOptions { bindings?: { db?: string; bucket?: string; kv?: string; email?: string } cors?: string[] skipCodegen?: boolean config?: string}| Opción | Por defecto | Qué hace |
|---|---|---|
bindings.db |
'DB' |
Nombre del binding de D1 del que sale el contenido |
bindings.bucket |
'R2' |
Binding de R2 donde se guardan los ficheros de las colecciones con upload |
bindings.kv |
'KV' |
Binding de KV, donde viven los ajustes del sitio bajo el prefijo settings: |
bindings.email |
'EMAIL' |
Binding de Email Sending, del que salen los correos de verificación y restablecimiento. Ver Email |
cors |
sin CORS | Orígenes permitidos en la API REST. Ver CORS |
skipCodegen |
false |
No regenera .cms/ al arrancar. Exige que ya exista |
config |
busca cms.config.{ts,js,mjs} en la raíz |
Ruta explícita al config |
Comprobaciones previas
Sección titulada «Comprobaciones previas»Se hacen en astro:config:setup, antes de cablear nada: un error al arrancar es infinitamente mejor
que un undefined a mitad de una petición. Cada fallo dice qué instalar y qué línea añadir.
| Requisito | Por qué |
|---|---|
@astrojs/cloudflare |
Los bindings solo existen en el runtime de Workers |
@astrojs/react |
El admin son islas de React |
output: 'server' |
El admin y la API se renderizan bajo demanda |
Que el tsconfig.json incluya ./.cms/types.d.ts es un aviso, no un error: sin él todo funciona,
solo pierdes el tipado de Astro.locals.cms.
Rutas inyectadas
Sección titulada «Rutas inyectadas»| Patrón | Qué sirve |
|---|---|
${admin.path}/[...path] |
El panel del admin |
${api.path}/[...path] |
Los seis endpoints de la API REST, más las rutas de ficheros |
/api/auth/[...all] |
La autenticación |
Los dos primeros patrones salen de tu config, así que mover el admin a /panel es cambiar una línea de
cms.config.ts. La de autenticación es fija. Las tres declaran prerender = false por su cuenta: tus
páginas estáticas siguen siendo estáticas.
El tema del panel
Sección titulada «El tema del panel»/admin sirve un documento HTML entero, así que la hoja de estilos de tu sitio no llega sola. admin.css
en cms.config.ts es una ruta —relativa a la raíz de tu proyecto de Astro— que el panel importa después
de la suya:
export default defineConfig({ collections: [posts], admin: { css: './src/styles/app.css' },})El panel define sus colores dentro de @layer base, así que cualquier declaración tuya sin capa le gana por
la regla de capas de la cascada, y si tú también usas @layer base ganas por llegar después. Si defines las
variables de shadcn —--background, --primary, --radius, las de --sidebar-*…— el panel se pinta con
las tuyas sin que tengas que tocar nada más.
Sus utilidades, en cambio, no son tuyas para pisar: viven en una capa propia, kevin-cms, declarada
después de utilities. Si no fuera así, tu hoja —que se importa la segunda— ganaría cualquier colisión de
nombre, incluidas las que nunca pediste: bastaría con que escribieras hidden en cualquier fichero para que
tu .hidden llegara después del @media de md:block del panel y le tumbara la barra lateral. Lo que sí es
un punto de extensión son los componentes de campo.
No hace falta que instales Tailwind ni que toques su config: dist/admin.css se publica ya compilado. Si la
ruta no existe, la integración falla al arrancar con ADMIN_CSS_NOT_FOUND en lugar de dejarte un error de
Vite en la primera petición.
Compartir el modo oscuro con tu sitio
Sección titulada «Compartir el modo oscuro con tu sitio»El panel lleva su propio conmutador: /admin sirve un <html> distinto del de tu sitio, así que la clase
.dark que tú pongas en tus páginas no le llega. Lo que sí comparten es el localStorage del origen, y el
paquete exporta la clave que usa:
import { THEME_KEY } from '@kevolution-co/cms'
export function ThemeToggle() { return ( <button onClick={() => { const dark = document.documentElement.classList.toggle('dark') localStorage.setItem(THEME_KEY, dark ? 'dark' : 'light') }} > Cambiar el tema </button> )}Con eso, cambiar el tema en tu sitio cambia también el del panel la próxima vez que se abra. Si prefieres que vayan por separado, usa cualquier otra clave y no pasa nada: son dos documentos independientes.
Para que no haya un parpadeo del tema claro antes del oscuro, la lectura tiene que correr antes del
primer pintado, así que va en un <script is:inline> dentro de <head> y no en una isla —cuando React
hidrata, la versión clara ya se ha pintado—. Envuélvelo en try/catch: localStorage lanza directamente
en un navegador con las cookies bloqueadas, y un tema no vale una página en blanco.
Astro.locals.cms
Sección titulada «Astro.locals.cms»Un middleware con order: 'pre' —para que el middleware de tu aplicación ya lo encuentre puesto— deja
en locals.cms las colecciones del config y la autenticación:
---export const prerender = false
const { docs } = await Astro.locals.cms.collections.posts.find({ where: { status: { equals: 'published' } } })---El tipado sale de .cms/types.d.ts, que augmenta App.Locals. No declaras nada.
La construcción es perezosa: locals.cms es un accesor y no se lee ningún binding hasta que algo
pide una colección. Una página que solo dibuja una portada estática no paga nada.
locals.cms.auth trae getSession(), getUser() y requireUser(). Su caché de sesión es por
petición, mientras que collections se construye una vez por isolate.
locals.cms.email trae send(), para mandar correo transaccional desde tus propias páginas. Ver
Email.
locals.cms.settings trae get() y update(), los ajustes del sitio
guardados en KV. Como email, es un accesor anidado: una página que no los lee no resuelve el
binding.
Codegen
Sección titulada «Codegen»Al arrancar, la integración escribe .cms/schema.ts y .cms/types.d.ts junto a tu cms.config.ts, y
registra ese fichero en el watcher: al guardarlo, Astro reinicia y se regeneran.
Generar o aplicar una migración nunca es automático. En dev, si no hay ninguna migración o si el
config se ha movido más allá de la última, lo avisa con el comando que toca.
Módulos virtuales
Sección titulada «Módulos virtuales»Las rutas y el middleware viven dentro del paquete, así que no pueden importar un fichero tuyo por ruta relativa. Cuatro módulos virtuales cierran el hueco.
virtual:kevin-cms/server lleva el config, el schema de Drizzle y los nombres de los bindings. No se
resuelve en el entorno del navegador, a propósito.
virtual:kevin-cms/config es lo que consume el admin, que corre en el navegador, y por eso lleva solo
la parte pública y serializable:
| Va | No va |
|---|---|
admin.path, admin.auth, api.path |
Los nombres de los bindings |
| Slug, etiquetas y opciones de admin de cada colección | access, que es la política de autorización |
| Nombre, tipo y validaciones de cada campo | Rutas absolutas de tu máquina |
upload y auth de la colección |
Un defaultValue que sea una función |
admin.css y admin.fieldComponents, que son rutas de build |
virtual:kevin-cms/theme es un import de tu admin.css, o export {} cuando no has declarado
ninguno. Ver El tema del panel.
virtual:kevin-cms/field-components es un import por cada componente de campo que hayas sustituido
con admin.fieldComponents, y un objeto con todos ellos; export default {} si no has sustituido
ninguno. Ver Poner el tuyo.
Ese último sí se resuelve en el navegador, a diferencia de virtual:kevin-cms/server: su contenido
son componentes de React, que es justo lo que el panel necesita allí. Las rutas se comprueban en
astro:config:setup, así que una que no existe es un error al arrancar y no un fallo de resolución de
Vite en la primera petición a /admin.