Instalación
Requisitos
Sección titulada «Requisitos»- Node 24.2 o superior.
- Una cuenta de Cloudflare.
- Un proyecto Astro con el adaptador de Cloudflare. Si no lo tienes,
cms initlo crea: solo hace falta si vas a cablear un proyecto que ya existe. - Un token de GitHub con permiso
read:packages— el paquete se publica en GitHub Packages, y GitHub Packages exige autenticación también para leer, aunque el paquete sea público. Se genera en Settings → Developer settings → Personal access tokens.
Arranca el registro
Sección titulada «Arranca el registro»Esto va antes del comando, y no dentro del proyecto: init tiene que saber de dónde bajar el
paquete, y el proyecto todavía no existe. Son dos líneas en tu ~/.npmrc —el global de tu usuario— y
la variable exportada:
@kevolution-co:registry=https://npm.pkg.github.com//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}export GITHUB_TOKEN=ghp_…${GITHUB_TOKEN} es texto literal dentro del fichero: lo expande npm desde tu entorno cada vez
que lo lee, así que el token no queda escrito en ningún sitio. Sin esto, el comando de abajo falla con
un 401.
Crea el proyecto con cms init
Sección titulada «Crea el proyecto con cms init»pnpm create @kevolution-co/cms mi-sitio --cloudCon npm, npm create @kevolution-co/cms -- mi-sitio --cloud —el -- es de npm, que si no se queda
los flags para sí—; con yarn y bun, yarn create @kevolution-co/cms … y bun create @kevolution-co/cms ….
Un comando, y al terminar tienes un proyecto Astro sobre Cloudflare con el CMS cableado, las dependencias instaladas, el secreto de sesión puesto y la primera migración aplicada en local.
create resuelve el paquete de arranque @kevolution-co/create-cms, que es cms init y nada más:
sin peers y sin un solo build script, así que instala limpio con pnpm 10 o superior. Es lo que
pnpm dlx @kevolution-co/cms init no podía hacer —arrastraba las peers de la librería y pnpm se
paraba en ERR_PNPM_IGNORED_BUILDS antes de ejecutar nada; ver
Solución de problemas—.
El resto de comandos viven en @kevolution-co/cms, que init deja instalado en el proyecto.
--cloud o --local
Sección titulada «--cloud o --local»La diferencia entre los dos modos son los cuatro wrangler … create, y nada más:
--cloudcrea en tu cuenta de Cloudflare la base D1, el bucket de R2 y los dos namespaces de KV, y deja los ids que Cloudflare emite escritos en elwrangler.jsonc.--localno toca tu cuenta. El proyecto sirve parapnpm dev,cms db:migrateycms db:applyen local —que no necesitan ids—, pero no se puede desplegar hasta que los subas concms resources:create.
El wrangler.jsonc nombra el bucket de R2 desde el primer momento, en los dos modos, porque R2 no
tiene id: su identidad es ese nombre. Que el bucket exista en tu cuenta es otra cosa, y solo la
sabe Cloudflare: cms resources:create se la pregunta antes de crear nada, así que lo crea si falta
y no hace nada si ya está (#173).
Si no pasas ninguno de los dos y hay terminal, el comando lo pregunta. Si no la hay —CI, o un agente—, exige uno de los dos y sale sin escribir nada: crear recursos de pago en una cuenta ajena no es una respuesta que se pueda dar por omisión.
Lo que deja escrito
Sección titulada «Lo que deja escrito»Para que reconozcas tu proyecto al abrirlo:
| Fichero | Qué lleva |
|---|---|
.npmrc |
Las dos líneas del registro, con ${GITHUB_TOKEN}. Va commiteado: lo necesita el build de Cloudflare |
package.json |
@kevolution-co/cms y sus peers —drizzle-orm, de tiempo de ejecución; drizzle-kit, de desarrollo— a la versión exacta que el paquete declara |
cms.config.ts |
Una colección posts de ejemplo y los valores de arranque del correo |
drizzle.config.ts |
sqlite, el schema en ./.cms/schema.ts y la salida en ./migrations |
astro.config.mjs |
output: 'server', el adaptador de Cloudflare, React y la integración del CMS |
wrangler.jsonc |
Los cinco bloques del CMS: D1, R2, los dos KV, send_email, nodejs_compat, migrations_pattern y assets.directory: "./dist/client" |
.gitignore |
.cms y .dev.vars |
tsconfig.json |
./.cms/types.d.ts dentro del include |
.dev.vars |
BETTER_AUTH_SECRET, generado con randomBytes(32). El valor no se imprime nunca |
migrations/ |
La primera migración, generada y aplicada en la D1 local |
No sobrescribe nada de lo que ya exista. Al terminar imprime qué escribió y qué se saltó porque ya
estaba, así que volver a ejecutarlo no cambia ni un byte. Si algo queda a medias, lo dice en amarillo
al final: cms doctor te enumera exactamente qué falta y con qué comando se arregla.
Los ocho flags —--cloud, --local, --name, --pm, --skip-create, --no-install,
--no-migrate y --yes— están en la referencia del CLI.
Arranca
Sección titulada «Arranca»pnpm devYa puedes leer contenido desde una plantilla, con los tipos de tus colecciones:
---export const prerender = false
const { docs } = await Astro.locals.cms.collections.posts.find({ limit: 5 })---
<ul>{docs.map((post) => <li>{post.title}</li>)}</ul>prerender = false es imprescindible en cualquier página que lea contenido: en build no hay bindings de
Cloudflare y no hay nada que leer.
También responden /api/cms/posts —los seis endpoints de la API REST— y
/admin, que en una instalación vacía te lleva al asistente de configuración: pide los ajustes del sitio
y la primera cuenta, y con eso cierra el registro.
Configura el email
Sección titulada «Configura el email»Enviar correo transaccional —verificación de cuenta, recuperación de contraseña— exige tres cosas:
- Un plan Workers de pago. Email Sending no está en el gratuito.
- Un dominio que use DNS de Cloudflare, dado de alta en Compute → Email Service → Email Sending.
- Que la dirección remitente pertenezca a ese dominio.
Y un bloque email en tu config, con esa dirección:
import { defineConfig } from '@kevolution-co/cms'
export default defineConfig({ email: { from: 'noreply@tudominio.com', siteName: 'Mi sitio' }, collections: [],})Este bloque son los valores de arranque. Los mismos tres se pueden cambiar después sin desplegar desde los ajustes del sitio, que viven en KV.
En desarrollo local no hace falta nada de esto: wrangler dev simula el envío y vuelca el contenido del
mensaje a un fichero cuya ruta imprime en la consola. Un from cualquiera vale mientras no sea vacío.
Si añades "remote": true al binding pierdes esa simulación y empiezas a enviar correo real desde tu
máquina, con los tres requisitos de arriba ya cumplidos. Déjalo fuera mientras desarrolles.
Los detalles están en Email.
Cómo se leen los bindings
Sección titulada «Cómo se leen los bindings»Desde Astro 7 y @astrojs/cloudflare 14, Astro.locals.runtime.env ya no existe. Los bindings se importan:
import { env } from 'cloudflare:workers'El CMS lo hace por ti; solo lo necesitas si quieres acceder a un binding directamente.
Si prefieres cablearlo a mano
Sección titulada «Si prefieres cablearlo a mano»Esta es la misma instalación paso a paso, y hace falta cuando cms init no puede hacerla por ti: un
proyecto Astro que ya existe y que el comando no sabe cablear sin riesgo —una astro.config.mjs con
una forma que no reconoce, un monorepo, un árbol con convenciones propias—. También sirve para saber
qué es cada cosa de las que el comando escribe.
Al terminar, cms doctor verifica el resultado paso a paso y dice qué falta y cómo arreglarlo.
Crea el proyecto e instala el paquete
Sección titulada «Crea el proyecto e instala el paquete»-
Crea la aplicación con la CLI de Cloudflare, eligiendo Astro y la plantilla en blanco:
Ventana de terminal pnpm create cloudflare@latest mi-sitio --framework=astro -
Añade React, que es lo que usa el administrador:
Ventana de terminal pnpm astro add react -
Apunta el scope
@kevolution-coa GitHub Packages, con un.npmrcen la raíz del proyecto:.npmrc @kevolution-co:registry=https://npm.pkg.github.com//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}${GITHUB_TOKEN}lo expande npm desde tu entorno al leer el fichero, así que el token no queda escrito en el repositorio. Sin este paso eladddel punto siguiente falla con un 401.Este fichero va commiteado: el build de Cloudflare lo necesita para instalar el paquete. Ver Despliegue.
-
Exporta el token e instala el CMS y
drizzle-orm:Ventana de terminal export GITHUB_TOKEN=ghp_…pnpm add @kevolution-co/cms drizzle-orm@1.0.0-rc.4drizzle-ormva en el mismo comando y sin-D: es una peer del paquete y es de tiempo de ejecución, no de desarrollo —el CMS la importa para hablar con D1, y el.cms/schema.tsque genera la importa también—. Y va con la versión, porque la peer es exacta: pedirla a secas te trae el dist-taglatest, que todavía es de la línea 0.45.pnpm peers checkse queja de las dos cosas —unmet peersi la instalas a secas,missing peersi no la instalas—, y cuando falta del todo el primer comando del CMS se para enCargando configcon unCannot find module 'drizzle-orm'.
Crea los recursos de Cloudflare a mano
Sección titulada «Crea los recursos de Cloudflare a mano»Cada comando imprime un identificador que hay que pegar en wrangler.jsonc.
npx wrangler d1 create mi-sitionpx wrangler r2 bucket create mi-sitionpx wrangler kv namespace create KVnpx wrangler kv namespace create SESSION{ "compatibility_flags": ["nodejs_compat"], "d1_databases": [ { "binding": "DB", "database_name": "mi-sitio", "database_id": "<id>", "migrations_dir": "migrations", "migrations_pattern": "migrations/*/migration.sql" } ], "r2_buckets": [{ "binding": "R2", "bucket_name": "mi-sitio" }], "kv_namespaces": [ { "binding": "SESSION", "id": "<id>" }, { "binding": "KV", "id": "<id>" } ], "send_email": [{ "name": "EMAIL" }]}Y el secreto con el que se firman las sesiones, que no es un binding sino una variable:
pnpm cms secret:setLo genera con randomBytes(32) y lo escribe en .dev.vars sin pisar nada de lo que ese fichero ya
tenga. El valor no se imprime: lo que dice el comando es dónde quedó.
En producción, pnpm cms secret:set --remote. .dev.vars va en el .gitignore. Los
detalles, en Autenticación.
Define tu primera colección
Sección titulada «Define tu primera colección»import { defineCollection, defineConfig } from '@kevolution-co/cms'
export default defineConfig({ collections: [ defineCollection({ slug: 'posts', admin: { useAsTitle: 'title' }, fields: [ { name: 'title', type: 'text', required: true }, { name: 'slug', type: 'text', required: true, unique: true, index: true }, { name: 'body', type: 'textarea' }, ], }), ],})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)],})output: 'server', el adaptador de Cloudflare y @astrojs/react son obligatorios: la integración
comprueba los tres al arrancar y falla nombrando el que falte. La referencia completa —opciones, rutas
inyectadas y qué pone en Astro.locals.cms— está en
Integración de Astro.
Genera y aplica las migraciones
Sección titulada «Genera y aplica las migraciones»Tu configuración no crea tablas por sí sola: produce migraciones SQL que revisas antes de aplicar.
Instala drizzle-kit, que es quien escribe el SQL, y añade su configuración:
pnpm add -D drizzle-kit@1.0.0-rc.4La versión va fijada, y no sobra: el CMS la declara como peer, y sin fijarla te llevas la última que
publica drizzle-kit, que es todavía de la línea 0.31 y no la satisface. Esta sí va con -D, al revés
que drizzle-orm: drizzle-kit solo escribe el SQL en tu máquina y no entra en lo que despliegas.
import { defineConfig } from 'drizzle-kit'
export default defineConfig({ dialect: 'sqlite', schema: './.cms/schema.ts', out: './migrations',})Genera:
pnpm cms db:migrate✔ Cargando config✔ Emitiendo .cms/3 colecciones desde /mi-sitio/cms.config.ts /mi-sitio/.cms/schema.ts /mi-sitio/.cms/types.d.tsSon tres colecciones y tú solo has declarado una. Las otras dos las añade el CMS por su cuenta:
users, la tabla de usuarios de la autenticación, y
ssd, la biblioteca de medios del panel.
Lee el SQL que ha salido en migrations/ y, si estás conforme, aplícalo:
pnpm cms db:applyAplicando en local sobre el binding DB🚣 13 commands executed successfully.No hace falta que le digas cuál es la base: la saca del binding de tu wrangler.jsonc. Si el SQL no te
convence, pnpm cms db:pop borra esa migración y vuelves a empezar.
Añade .cms/ a tu .gitignore —se regenera siempre— y commitea migrations/.
La integración vuelve a generar .cms/ en cada arranque y vigila tu cms.config.ts, así que en
desarrollo no tienes que llamar a db:generate a mano. Lo que nunca es automático es generar o aplicar
una migración: eso siempre lo pides tú.
Por último, deja que TypeScript vea los tipos generados:
{ "include": [ ".astro/types.d.ts", "./.cms/types.d.ts", "**/*" ]}Hay que nombrarlo: un comodín no entra en un directorio cuyo nombre empieza por punto. Con esa línea,
Astro.locals.cms.collections.posts queda tipado con tus colecciones sin que declares nada más.
El ciclo completo, con el aviso de deriva y lo que SQLite no te deja hacer, está en Schema y migraciones.