Ir al contenido

Instalación

  • Node 24 o superior.
  • Una cuenta de Cloudflare.
  • Un proyecto Astro con el adaptador de Cloudflare. Si no lo tienes, el primer paso lo crea.
  • 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.
  1. 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
  2. Añade React, que es lo que usa el administrador:

    Ventana de terminal
    pnpm astro add react
  3. Apunta el scope @kevolution-co a GitHub Packages, con un .npmrc en 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 el add del punto siguiente falla con un 401.

  4. Exporta el token e instala el CMS:

    Ventana de terminal
    export GITHUB_TOKEN=ghp_
    pnpm add @kevolution-co/cms

Cada comando imprime un identificador que hay que pegar en wrangler.jsonc.

Ventana de terminal
npx wrangler d1 create mi-sitio
npx wrangler r2 bucket create mi-sitio
npx wrangler kv namespace create KV
npx wrangler kv namespace create SESSION
wrangler.jsonc
{
"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:

Ventana de terminal
openssl rand -base64 32
.dev.vars
BETTER_AUTH_SECRET=el-valor-que-acabas-de-generar

En producción, npx wrangler secret put BETTER_AUTH_SECRET. .dev.vars va en el .gitignore. Los detalles, en Autenticación.

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.

Enviar correo transaccional —verificación de cuenta, recuperación de contraseña— exige tres cosas:

  1. Un plan Workers de pago. Email Sending no está en el gratuito.
  2. Un dominio que use DNS de Cloudflare, dado de alta en Compute → Email Service → Email Sending.
  3. Que la dirección remitente pertenezca a ese dominio.

Y un bloque email en tu config, con esa dirección:

cms.config.ts
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.

cms.config.ts
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' },
],
}),
],
})
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)],
})

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.

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:

Ventana de terminal
pnpm add -D drizzle-kit
drizzle.config.ts
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
dialect: 'sqlite',
schema: './.cms/schema.ts',
out: './migrations',
})

Genera:

Ventana de terminal
pnpm cms db:migrate
✔ Cargando config
✔ Emitiendo .cms/
2 colecciones desde /mi-sitio/cms.config.ts
/mi-sitio/.cms/schema.ts
/mi-sitio/.cms/types.d.ts

Son dos colecciones y tú solo has declarado una: users la añade el CMS por su cuenta, porque es la tabla de usuarios de la autenticación.

Lee el SQL que ha salido en migrations/ y, si estás conforme, aplícalo:

Ventana de terminal
pnpm cms db:apply
Aplicando en local sobre el binding DB
🚣 8 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:

tsconfig.json
{
"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.

Ventana de terminal
pnpm dev

Ya puedes leer contenido desde una plantilla, con los tipos de tus colecciones:

src/pages/index.astro
---
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.