Ir al contenido

Ajustes

Hay un puñado de valores que no son contenido pero tampoco código: el nombre del sitio, su URL pública, la dirección desde la que sale el correo. Cambiarlos no debería exigir un despliegue.

Viven en Cloudflare KV, con una clave por ajuste —settings:site:<campo>, por ejemplo settings:site:siteUrl—, y se leen desde Astro.locals.cms.settings o por la API REST. Que cada ajuste tenga su clave es lo que hace que dos guardados a la vez de campos distintos ya no se pisen: antes vivían todos en un único blob JSON bajo settings:site, y el segundo en escribir borraba lo que el primero acababa de guardar. Ese blob antiguo no hace falta migrarlo: cada campo que todavía no tenga clave propia se sigue leyendo de ahí hasta que se vuelva a guardar.

Campo Por defecto Quién lo usa
siteName email.siteName del config ('Kevin CMS') Firma las plantillas de correo
emailFrom email.from del config Cabecera From de todo el correo
emailReplyTo email.replyTo del config Cabecera Reply-To. Vacío significa sin cabecera
siteUrl '' La URL base de better-auth. Vacía significa deducirla de la petición
favicon '' El icono del panel. Vacío significa el que trae Kevin CMS
logo '' Nada todavía. Se guarda y se sirve, y el panel sigue pintando su propia marca
locale 'es' Nada todavía. Se guarda y se sirve

favicon y logo son URLs, y los dos se rellenan igual desde /admin/settings: eliges un fichero, la pantalla lo sube a .system —la carpeta reservada de la biblioteca de medios, que no se lista ni se navega— y lo que se guarda en KV es la URL que devuelve, de la forma /api/cms/cdn/ssd/<id>/<fichero>. Cambiar uno borra el fichero al que el ajuste dejó de apuntar, si de verdad era suyo: lo decide la carpeta de la fila, no cómo esté escrita la URL.

Esta es la regla que ordena todo lo demás:

cms.config.ts → valores por defecto ⊕ lo guardado en KV = ajustes efectivos

Lo que escribes en el bloque email de tu config es el valor de arranque. Lo que se guarda en KV lo pisa. Y lo guardado es disperso: solo las claves que alguien editó de verdad.

Esa última parte importa más de lo que parece. Si guardara el objeto entero, la primera escritura congelaría dentro de KV los valores que venían del config, y a partir de ahí cambiar siteName en cms.config.ts dejaría de surtir efecto aunque nadie hubiera tocado ese campo jamás. Siendo disperso, cada campo sigue al config hasta el día que se edita, y solo desde ese día manda KV.

---
export const prerender = false
const { siteName, siteUrl } = await Astro.locals.cms.settings.get()
---
<footer>{siteName}</footer>

get() nunca falla. Si no hay nada guardado, si el blob antiguo está corrupto, si un campo trae el tipo equivocado o si siteUrl no es una URL válida, ese campo cae a su valor por defecto y el problema se registra en la consola. Unos ajustes rotos no pueden tumbar el sitio entero.

await Astro.locals.cms.settings.update({ siteName: 'Mi sitio' })

Es una fusión superficial: los campos que el patch no trae conservan su valor. Guardar el formulario de ajustes generales no debe tocar campos que ese formulario ni siquiera muestra.

La validación es estricta. Solo se aceptan esos siete campos, todos los valores tienen que ser cadenas, y un siteUrl no vacío tiene que ser una URL absoluta http(s) sin espacios ni saltos de línea. siteUrl es el único con regla de formato. favicon y logo admiten cualquier cadena: por esta vía puedes escribir /logo.svg de tu propio public/, una URL completa de otro dominio o la de un fichero que hayas subido tú. Exigir la URL de un fichero subido impediría lo más común, y subir el fichero es trabajo de la pantalla de Ajustes, no de la API. Un incumplimiento es un error de validación con todos los problemas a la vez, no solo el primero — y por REST, un 400 con su array issues.

Petición Respuesta
GET /api/cms/settings 200 con los siete campos efectivos
PATCH /api/cms/settings 200 con el resultado de la fusión
Cualquiera de las dos sin sesión 401
Cualquier otro método 405

Las dos exigen sesión, sin excepción: no hay equivalente al access.read: 'public' de las colecciones, y emailFrom no es algo que dar a quien pregunte sin identificarse.

Los ajustes se leen en casi todas las peticiones y no se escriben casi nunca, que es exactamente el perfil para el que sirve KV. Los documentos son al revés, y por eso viven en D1.

El precio es la consistencia eventual: una escritura tarda hasta un minuto en verse en todas partes. Es tolerable para el nombre del sitio e intolerable para un artículo que el autor acaba de guardar.

Los ajustes no están solos en ese namespace: los globals lo comparten, con el prefijo global: en vez de settings:, y por la misma razón. El CMS nunca lee, lista ni borra fuera de esos dos prefijos, así que el namespace puede ser el mismo que ya uses para otra cosa.

Una sola, en el ámbito del isolate, con 60 segundos de vida — el mismo minuto que tarda KV en propagar, ni más ni menos. No tendría sentido prometer una frescura que KV no puede cumplir.

Lo que guarda es la promesa y no el valor, así que dos lecturas concurrentes comparten una sola consulta a KV en vez de lanzar dos.

update() sustituye la entrada en el isolate que escribe. Quien guarda ve su cambio de inmediato; los demás esperan la propagación.

Cosa Estado
Credenciales de OAuth en settings:auth-providers ❌ Llegan con los proveedores sociales
Que locale haga algo ❌ Se guarda y se sirve, nada más
Más campos que estos siete ❌ Un modelo de contenido singular de verdad son los globals