Ir al contenido

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.

Ventana de terminal
pnpm create @kevolution-co/cms mi-sitio --cloud

Si 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:

Ventana de terminal
pnpm dlx --ignore-scripts @kevolution-co/cms init mi-sitio --cloud

Dentro 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 E401
npm error 401 Unauthorized - GET https://npm.pkg.github.com/@kevolution-co%2fcms

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

  1. Falta el .npmrc del scope, o no apunta a https://npm.pkg.github.com. Si estás ejecutando pnpm create @kevolution-co/cms, el que hace falta es el global (~/.npmrc): el del proyecto todavía no existe.
  2. El token no tiene read:packages. Es el único permiso que hace falta, y ninguno lo sustituye.
  3. El token no está exportado en la shell desde la que lanzas el comando.

Arreglo. Las dos líneas y la variable:

~/.npmrc
@kevolution-co:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
Ventana de terminal
export GITHUB_TOKEN=ghp_

El token se genera en Settings → Developer settings → Personal access tokens. Ver Instalación.

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:

Ventana de terminal
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:

Ventana de terminal
env -u GITHUB_TOKEN cms deploy # lanza el comando sin esa variable
GH_TOKEN=<uno con permiso repo> cms deploy

Comprueba 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:

wrangler.jsonc
{
"d1_databases": [
{
"binding": "DB",
"database_name": "mi-sitio",
"database_id": "<id>",
"migrations_dir": "migrations",
"migrations_pattern": "migrations/*/migration.sql"
}
]
}

Ver Instalación.

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.

Ventana de terminal
pnpm cms db:migrate
pnpm cms db:apply

Encadenarlos en un solo comando es justo lo que la CLI evita a propósito. Ver CLI.

Síntoma. El primer comando del CMS que carga tu cms.config.tscms db:generate, cms db:migrate— se para nada más empezar:

- Cargando config
✖ Cargando config
Cannot find module 'drizzle-orm'
Require stack:
- /mi-sitio/node_modules/.pnpm/@kevolution-co+cms@…/node_modules/@kevolution-co/cms/dist/….js

pnpm 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:

Ventana de terminal
pnpm add drizzle-orm@1.0.0-rc.4

Con 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ón
va fijada a propósito: es la que el CMS declara como peer, y la última que publica drizzle-kit
es 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:

Ventana de terminal
pnpm add -D drizzle-kit@1.0.0-rc.4
drizzle.config.ts
import { 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:

wrangler.jsonc
{
"kv_namespaces": [
{ "binding": "SESSION", "id": "<id>" },
{ "binding": "KV", "id": "<id>" }
]
}

Ver Instalació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.

wrangler.jsonc
{ "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:

wrangler.jsonc
{ "send_email": [{ "name": "EMAIL" }] }
cms.config.ts
import { defineConfig } from '@kevolution-co/cms'
export default defineConfig({
email: { from: 'noreply@tudominio.com', siteName: 'Mi sitio' },
collections: [],
})

Ver Email.

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.com
To: kevin@example.com
Subject: Verifica tu cuenta en Kevin CMS
Text: /tmp/miniflare-…/email-text/<id>.txt

Arreglo. 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—:

wrangler.jsonc
{ "send_email": [{ "name": "EMAIL", "remote": true }] }

Desplegado no hay simulación que valga: ahí el envío es real siempre. Ver Email y Despliegue.

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:

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

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

Causa. Una de dos, y las dos son de la máquina del builder, no de la tuya:

  1. El .npmrc no llegó al repositorio, normalmente porque tu .gitignore lo ignora. Ese fichero va commiteado a propósito: no lleva el token, lleva ${GITHUB_TOKEN}.
  2. Falta GITHUB_TOKEN como secret de build en el proyecto de Cloudflare, o no tiene permiso read: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:

Ventana de terminal
git check-ignore -v .npmrc

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

wrangler.jsonc
{ "name": "mi-sitio" }

Es el paso 4 de los cuatro clics, y es el que más veces se pasa por alto.

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:

  1. Las migraciones no se han aplicado en remoto. La D1 de producción es otra base: el cms db:apply que corriste en local no la tocó, así que el Worker consulta tablas y columnas que ahí no existen.
  2. Falta BETTER_AUTH_SECRET en el Worker. .dev.vars es solo local y no viaja en el deploy.

Arreglo.

Ventana de terminal
pnpm cms db:apply --remote # desde tu máquina, nunca en el build command
pnpm cms secret:set --remote # si no lo habías subido

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