Solución de problemas
Lo que tienes es un síntoma: un mensaje de error o algo que no pasa. Así está ordenada esta página. Busca el tuyo, y debajo tienes la causa y el arreglo.
Casi todos salen de la configuración, no del código, y casi todos se arreglan con una línea.
ERR_PNPM_IGNORED_BUILDS al arrancar con pnpm dlx
Sección titulada «ERR_PNPM_IGNORED_BUILDS al arrancar con pnpm dlx»Síntoma. El primer comando aborta antes de crear nada, y pnpm approve-builds responde que no
hay nada que aprobar:
$ pnpm dlx @kevolution-co/cms init mi-sitio --local
Error: ERR_PNPM_IGNORED_BUILDS × adding a new package ╰─▶ Ignored build scripts: esbuild@0.28.2, workerd@1.20260915.1 help: Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.Causa. cms init no ha llegado a ejecutarse. Es pnpm dlx quien falla al instalar el CLI:
instala las peers de @kevolution-co/cms —astro, wrangler, drizzle-kit— y esas traen esbuild y
workerd, cuyos build scripts pnpm 10 o superior se niega a ejecutar sin aprobación explícita. Y
pnpm approve-builds no puede darla, porque mira el proyecto actual y el árbol que falló está en la
caché temporal del dlx. El binario no necesita ninguno de esos paquetes: son las dependencias de
la librería, no del comando.
Arreglo. Arrancar por el paquete de arranque, que es lo que documenta la
instalación: solo init, sin peers y sin
build scripts, así que instala limpio sin flags.
pnpm create @kevolution-co/cms mi-sitio --cloudSi insistes en dlx —por ejemplo para ejecutar otro comando fuera de un proyecto—, la salida de
emergencia es no ejecutar los build scripts, que el CLI no usa:
pnpm dlx --ignore-scripts @kevolution-co/cms init mi-sitio --cloudDentro de un proyecto la aprobación sí tiene dónde declararse, y pnpm sí la lee ahí: el
allowBuilds del pnpm-workspace.yaml de la raíz, con esbuild: true y workerd: true, que
create-cloudflare deja escrito al crear el proyecto. En la caché de un dlx no hay raíz que la
declare, y eso es lo que el paquete de arranque evita.
401 Unauthorized al instalar @kevolution-co/cms
Sección titulada «401 Unauthorized al instalar @kevolution-co/cms»Síntoma. El add del paquete se cae nada más empezar, en tu máquina:
npm error code E401npm error 401 Unauthorized - GET https://npm.pkg.github.com/@kevolution-co%2fcmsCausa. El paquete se publica en GitHub Packages, que exige autenticación también para leer.
Sólo hay tres formas de que salga ese 401, y conviene descartarlas en este orden:
- Falta el
.npmrcdel scope, o no apunta ahttps://npm.pkg.github.com. Si estás ejecutandopnpm create @kevolution-co/cms, el que hace falta es el global (~/.npmrc): el del proyecto todavía no existe. - El token no tiene
read:packages. Es el único permiso que hace falta, y ninguno lo sustituye. - El token no está exportado en la shell desde la que lanzas el comando.
Arreglo. Las dos líneas y la variable:
@kevolution-co:registry=https://npm.pkg.github.com//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}export GITHUB_TOKEN=ghp_…El token se genera en Settings → Developer settings → Personal access tokens. Ver Instalación.
Failed to replace env in config
Sección titulada «Failed to replace env in config»Síntoma. El gestor no llega ni a pedir el paquete:
npm error Failed to replace env in config: ${GITHUB_TOKEN}Causa. El .npmrc está bien: lo que falta es la variable. ${GITHUB_TOKEN} es texto literal
dentro del fichero y npm lo expande de tu entorno al leerlo; si ahí no hay nada que expandir, se
para con ese mensaje. Pasa siempre en una terminal nueva, o en un sudo que no arrastra el entorno.
Arreglo. Exportarla y repetir el comando:
export GITHUB_TOKEN=ghp_…Si la usas a diario, déjala en el perfil de tu shell en vez de escribirla cada vez.
gh no puede autenticarse con GITHUB_TOKEN exportado
Sección titulada «gh no puede autenticarse con GITHUB_TOKEN exportado»Síntoma. cms deploy se para diciendo que gh está instalado pero no puede autenticarse, y tú
tienes la sesión de gh perfectamente iniciada. gh auth status también se queja.
Causa. Es la trampa de esta instalación, y no es culpa tuya: gh prefiere GITHUB_TOKEN (o
GH_TOKEN) sobre su propio llavero, y la instalación del CMS te pide exportar justo esa variable
para que npm pueda bajar el paquete del registro privado. Ese token es de paquetes: con
read:packages no se crea un repositorio, así que gh queda inútil con la sesión intacta. Ejecutar
gh auth login no arregla nada, porque el problema es la variable.
Arreglo. Cualquiera de las dos:
env -u GITHUB_TOKEN cms deploy # lanza el comando sin esa variableGH_TOKEN=<uno con permiso repo> cms deployComprueba cuál está usando gh con gh auth status. Ver Despliegue.
«No migrations to apply!» y las migraciones están ahí
Sección titulada ««No migrations to apply!» y las migraciones están ahí»Síntoma. wrangler d1 migrations apply —o cms db:apply, que lo llama por dentro— dice que
no hay nada que aplicar, con el directorio migrations/ lleno de carpetas delante.
Causa. Falta migrations_pattern en tu wrangler.jsonc. drizzle-kit 1.x escribe
migrations/<fecha>_<nombre>/migration.sql, un nivel más hondo que el migrations/*.sql que wrangler
busca por defecto. Wrangler mira donde le dijeron y no encuentra nada, que es la verdad desde su punto
de vista.
Arreglo. Una línea en el bloque de D1:
{ "d1_databases": [ { "binding": "DB", "database_name": "mi-sitio", "database_id": "<id>", "migrations_dir": "migrations", "migrations_pattern": "migrations/*/migration.sql" } ]}Ver Instalación.
«No hay ninguna migración que aplicar»
Sección titulada ««No hay ninguna migración que aplicar»»Síntoma. Este mensaje, que es de la CLI y no de wrangler:
No hay ninguna migración que aplicar. Ejecuta `cms db:migrate` para generarla.Causa. Distinta de la anterior: aquí el directorio migrations/ está vacío de verdad. Has
lanzado db:apply antes de generar nada.
Arreglo. Son dos comandos, siempre en este orden: db:migrate escribe el SQL, tú lo lees,
db:apply lo aplica.
pnpm cms db:migratepnpm cms db:applyEncadenarlos en un solo comando es justo lo que la CLI evita a propósito. Ver CLI.
Cannot find module 'drizzle-orm'
Sección titulada «Cannot find module 'drizzle-orm'»Síntoma. El primer comando del CMS que carga tu cms.config.ts —cms db:generate,
cms db:migrate— se para nada más empezar:
- Cargando config✖ Cargando configCannot find module 'drizzle-orm'Require stack:- /mi-sitio/node_modules/.pnpm/@kevolution-co+cms@…/node_modules/@kevolution-co/cms/dist/….jspnpm peers check lo dice antes, y más claro:
Issues with peer dependencies found
✕ missing peer drizzle-orm Wanted: 1.0.0-rc.4: @kevolution-co/cms@<versión>Causa. Falta drizzle-orm. Es una peer no opcional del CMS, y las peers las instalas tú: el
paquete la importa para hablar con D1 y el .cms/schema.ts generado la importa también, así que
tiene que haber una sola copia y vive en tu proyecto, no dentro del nuestro. Ese mensaje es de Node,
no del CMS —por eso señala un fichero del dist/ en vez de decirte qué instalar.
Arreglo. Instálala como dependencia normal —no con -D, que el CMS la necesita también en
producción— y con la versión que declara la peer:
pnpm add drizzle-orm@1.0.0-rc.4Con la versión ya puesta, pnpm peers check puede seguir diciendo unmet peer drizzle-orm, pero con
un Wanted: ^0.45.2 a nombre de better-auth. Eso ya no es esta avería: better-auth va dentro
del paquete y todavía declara la línea 0.45, así que es un aviso de rango y no una copia que falte.
La versión que instalar sigue siendo la que declara nuestra peer, que es con la que el CMS se
desarrolla y se prueba.
Ver Instalación.
db:migrate falla diciendo que drizzle-kit no está
Sección titulada «db:migrate falla diciendo que drizzle-kit no está»Síntoma.
drizzle-kit no está instalado y `cms db:migrate` lo necesita para generar la migración.Instálalo con `pnpm add -D drizzle-kit@1.0.0-rc.4` (o el equivalente de tu gestor). La versiónva fijada a propósito: es la que el CMS declara como peer, y la última que publica drizzle-kites todavía de la línea 0.31.Causa. drizzle-kit es quien escribe el SQL, y no viaja dentro del paquete: lo lanzamos como
subproceso para no arrastrarlo al node_modules de producción de todo el que instale el CMS.
Arreglo. Instálalo como dependencia de desarrollo —con la versión, no a secas— y añade su config si aún no la tienes:
pnpm add -D drizzle-kit@1.0.0-rc.4import { defineConfig } from 'drizzle-kit'
export default defineConfig({ dialect: 'sqlite', schema: './.cms/schema.ts', out: './migrations',})Ver Schema y migraciones.
env.KV es undefined y el binding estaba declarado
Sección titulada «env.KV es undefined y el binding estaba declarado»Síntoma. El sitio arranca, pero no se puede guardar nada de lo que vive en KV: los ajustes del
sitio y los globals. Las lecturas siguen respondiendo
con los valores por defecto y un aviso en la consola; lo que falla son las escrituras, con
MISSING_KV. Tu wrangler.jsonc declara KV y aun así no está.
Causa. Falta SESSION en kv_namespaces. Ese namespace no es del CMS: es de
@astrojs/cloudflare, que guarda ahí las sesiones de Astro. Cuando no lo encuentra, el adaptador
sustituye el array entero por el suyo, y KV desaparece sin un solo aviso.
Arreglo. Declara los dos, aunque tú solo uses uno:
{ "kv_namespaces": [ { "binding": "SESSION", "id": "<id>" }, { "binding": "KV", "id": "<id>" } ]}Ver Instalación.
Un error de node:crypto al iniciar sesión
Sección titulada «Un error de node:crypto al iniciar sesión»Síntoma. El sitio arranca y el contenido se lee, pero cualquier ruta de autenticación revienta con un error de módulo de Node.
Causa. Falta la marca nodejs_compat. La autenticación es better-auth, y better-auth usa
node:crypto, que en el runtime de Workers no existe sin esa marca.
Arreglo.
{ "compatibility_flags": ["nodejs_compat"] }Ver Autenticación.
MISSING_EMAIL en la primera lectura de sesión
Sección titulada «MISSING_EMAIL en la primera lectura de sesión»Síntoma. El contenido y la API REST funcionan, pero en cuanto algo lee la sesión salta
MISSING_EMAIL. Nadie puede autenticarse.
Causa. El correo no está configurado, y la verificación de la dirección es obligatoria. El error
es perezoso —no tumba el arranque— y aparece cuando de verdad hace falta mandar un correo. Alguno de
estos falta: el binding send_email, un email.from no vacío, o
los tres requisitos de plataforma: plan Workers
de pago, dominio con DNS de Cloudflare dado de alta en Email Sending, y que from pertenezca a ese
dominio.
Arreglo. El propio mensaje enumera los tres requisitos, porque saber solo que «falta el binding»
manda a mirar al sitio equivocado dos de cada tres veces. En local basta con el binding y un from
cualquiera que no sea vacío:
{ "send_email": [{ "name": "EMAIL" }] }import { defineConfig } from '@kevolution-co/cms'
export default defineConfig({ email: { from: 'noreply@tudominio.com', siteName: 'Mi sitio' }, collections: [],})Ver Email.
El correo nunca llega a la bandeja
Sección titulada «El correo nunca llega a la bandeja»Síntoma. El flujo de verificación o de restablecimiento se completa sin errores, pero al buzón no llega nada.
Causa. En local es lo esperado, no un fallo: sin "remote": true, wrangler dev simula el
envío. Escribe el mensaje en un fichero temporal e imprime su ruta en la consola.
[wrangler:info] send_email binding called with MessageBuilder:From: noreply@example.comTo: kevin@example.comSubject: Verifica tu cuenta en Kevin CMSText: /tmp/miniflare-…/email-text/<id>.txtArreglo. Abre ese fichero y sigue el enlace: los flujos completos se desarrollan así, sin dominio
ni plan de pago. Si lo que quieres es mandar correo de verdad desde tu máquina, añade "remote": true
al binding —y cumple antes los tres requisitos de plataforma—:
{ "send_email": [{ "name": "EMAIL", "remote": true }] }Desplegado no hay simulación que valga: ahí el envío es real siempre. Ver Email y Despliegue.
401 en un GET /api/cms/<slug>
Sección titulada «401 en un GET /api/cms/<slug>»Síntoma.
{ "error": "UNAUTHORIZED", "message": "Esta operación sobre \"posts\" requiere una sesión iniciada"}Causa. Leer por la API REST exige sesión salvo que la colección —o el
global— diga lo contrario. Sin access: { read: 'public' }, un GET sin
cookie es 401.
Arreglo. Si esa colección se lee sin sesión —un blog, un catálogo—, decláralo:
defineCollection({ slug: 'posts', access: { read: 'public' }, fields: [{ name: 'title', type: 'text', required: true }],})Un GET /api/cms/globals/<slug> responde el mismo 401 con el mismo mensaje, y la palanca es la
misma sobre defineGlobal: access: { read: 'public' }. Ver
Declararlo.
'public' es el único valor admitido; cualquier otro es un error de configuración al arrancar.
Y esto solo abre las lecturas: escribir sigue exigiendo sesión siempre.
Ver API REST.
Astro.locals.cms.collections.posts no está tipado
Sección titulada «Astro.locals.cms.collections.posts no está tipado»Síntoma. El editor no autocompleta tus colecciones, o posts sale como error de tipo, aunque
.cms/types.d.ts exista con todo dentro.
Causa. El fichero no está incluido en tu tsconfig.json. Un comodín no entra en un directorio
cuyo nombre empieza por punto, así que **/* no lo alcanza: hay que nombrarlo.
Arreglo.
{ "include": [ ".astro/types.d.ts", "./.cms/types.d.ts", "**/*" ]}La integración lo comprueba al arrancar y lo dice, pero como aviso y no como error: sin esa línea todo funciona, solo pierdes el tipado. Ver Integración de Astro.
El build de Cloudflare falla con un 401 al instalar
Sección titulada «El build de Cloudflare falla con un 401 al instalar»Síntoma. En tu máquina todo instala, pero el build de Workers Builds se cae:
ERR_PNPM_FETCH_401 GET https://npm.pkg.github.com/@kevolution-co%2fcms: UnauthorizedCausa. Una de dos, y las dos son de la máquina del builder, no de la tuya:
- El
.npmrcno llegó al repositorio, normalmente porque tu.gitignorelo ignora. Ese fichero va commiteado a propósito: no lleva el token, lleva${GITHUB_TOKEN}. - Falta
GITHUB_TOKENcomo secret de build en el proyecto de Cloudflare, o no tiene permisoread:packages. No es una variable de entorno cualquiera: se pone en Build variables and secrets, como secret.
Arreglo. Las dos cosas se comprueban antes de empujar, y de eso ya se encargan cms doctor —con
los checks npmrc-ignored y npmrc-token— y cms deploy, que se niega a seguir. Para arreglarlo:
quita la línea .npmrc de tu .gitignore, mete el fichero en el repositorio y commitéalo. Que no
esté ignorado se comprueba así, y lo correcto es que no imprima nada:
git check-ignore -v .npmrcY en el dashboard, en Build variables and secrets, GITHUB_TOKEN como secret de build con
permiso read:packages. Ver Despliegue.
El Worker desplegado no es el mío, o sus bindings están vacíos
Sección titulada «El Worker desplegado no es el mío, o sus bindings están vacíos»Síntoma. El build de Cloudflare termina en verde, pero el sitio que ves no es el tuyo, o arranca y todo lo que toca D1, R2 o KV falla como si los bindings no existieran. En el dashboard aparecen dos Workers.
Causa. El nombre del Worker en Cloudflare no coincide con el name de tu wrangler.jsonc.
Cloudflare creó entonces un Worker distinto, y los bindings declarados en ese fichero —que se resuelven
por nombre— no apuntan a nada.
Arreglo. Que los dos nombres sean exactamente el mismo, en cualquiera de las dos direcciones:
renombrar el Worker en el dashboard, o cambiar el name del wrangler.jsonc y volver a empujar.
{ "name": "mi-sitio" }Es el paso 4 de los cuatro clics, y es el que más veces se pasa por alto.
Desplegué y /admin responde 500
Sección titulada «Desplegué y /admin responde 500»Síntoma. El Worker está arriba y las páginas públicas responden, pero /admin —o cualquier ruta
que lea la sesión— devuelve un 500.
Causa. Casi siempre una de dos, en este orden de probabilidad:
- Las migraciones no se han aplicado en remoto. La D1 de producción es otra base: el
cms db:applyque corriste en local no la tocó, así que el Worker consulta tablas y columnas que ahí no existen. - Falta
BETTER_AUTH_SECRETen el Worker..dev.varses solo local y no viaja en el deploy.
Arreglo.
pnpm cms db:apply --remote # desde tu máquina, nunca en el build commandpnpm cms secret:set --remote # si no lo habías subidoY por qué db:apply --remote no va en el build command de Cloudflare: no está verificado que el
token del builder pueda editar D1, un fallo a mitad de build deja el Worker sirviendo contra un
esquema que no existe, y el build es justo el sitio donde una confirmación sin terminal se acepta sin
que nadie mire. Las tres razones, con su consecuencia operativa —cada cambio de esquema es un
cms db:apply --remote antes del push siguiente—, están en
Despliegue.