Ir al contenido

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.

astro.config.mjs
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.

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

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.

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.

/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.

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.

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.

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.

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 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.