El servicio de systemd nunca llegó a instalarse y el sitio de fábrica de
Nginx seguía puesto: es él quien contesta a quien entra por la IP, así
que se veía su bienvenida aunque nuestro sitio estuviera bien
configurado.
`instalar.sh` hace las tres cosas que el despliegue normal no puede hacer
por sí solo —poner el unit, poner el sitio, quitar el de fábrica—, es
idempotente, y comprueba al final que Node y Nginx contestan de verdad en
vez de dar por hecho que sí.
El script va dentro de `despliegue/`, que ahora se sube con el resto: la
vez anterior remití a copiar de una ruta del servidor donde ese archivo
no estaba.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cambia las referencias al dominio de trabajo: la URL canónica, los correos
de los textos legales, las direcciones de retorno de Google y Facebook, y
lo que espera la suite.
El `robots.txt` pasa de archivo estático a ruta generada. Llevaba el
dominio escrito a mano y se quedó anunciando el anterior: un mapa del
sitio mal anunciado no da ningún error, simplemente no lo encuentra
nadie. Ahora sale de `site.url`, igual que el mapa y las etiquetas de
cada página. De paso deja fuera del rastreo lo que depende de quién mira
—cuenta, carrito, favoritos, entrar, descargas—, que no es contenido.
Y el Nginx queda listo para el dominio, con los tres pasos que hay que
dar fuera del servidor: los registros A a 89.46.247.16, la redirección de
los puertos 80 y 443 a 192.168.18.171, y Certbot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Detrás de la cuenta hay compras y facturación, y un código por correo apoya
toda esa puerta en el buzón: quien entre ahí, entra aquí. Delegar la
identidad en un proveedor que ya tiene doble factor la sostiene mejor.
Es el flujo de código de autorización con PKCE, escrito a mano porque son
cien líneas y las partes que importan son tres, mejor a la vista que
detrás de una dependencia: comparar el `state`, mandar el `code_verifier`
y exigir que el correo venga verificado antes de enlazar nada.
Lo que NO hace, a propósito: verificar la firma del `id_token`. La
identidad no se saca de lo que trae el navegador, sino de una llamada
nuestra al proveedor por TLS, así que no hay firma ajena que comprobar.
Y no guarda ningún token: se usa una vez para preguntar quién es y se
tira.
El enlazado por correo verificado es lo delicado, y por eso está separado
en `usuarioParaPerfil` con su explicación: sin exigir la verificación,
cualquiera podría poner la dirección de otro en un perfil suyo y quedarse
con su cuenta y sus compras.
El código por correo se queda debajo, como alternativa. Sin credenciales
puestas, los botones no se pintan y la ruta contesta 503: un botón que
solo lleva a un error es peor que no tenerlo.
Y los textos legales al día, que ahora sí hay terceros: qué se manda a
Google y a Facebook, cuándo, qué se guarda y cómo revocarlo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El acceso deja de ser un salto de página: «Entrar» abre un diálogo con la
imagen de marca a la izquierda y el formulario a la derecha, y al entrar
se vuelve a donde se estaba en vez de aterrizar en /cuenta. `/entrar`
sigue existiendo con el mismo formulario y la misma estampa, para quien
llegue por un enlace directo o sin JavaScript.
Tres cosas que salieron de probarlo, no de suponerlo:
- `showModal()` enfocaba el campo trampa contra envíos automáticos: está
fuera de pantalla y fuera del recorrido de tabulación, pero se puede
enfocar. Ahora el campo se enfoca a mano.
- Una regla de `display` en el diálogo le gana a la del navegador, que es
hoja de usuario. El diálogo cerrado se pintaba igual: una pantalla de
vacío al final de todas las páginas del sitio.
- Los enlaces legales navegaban por debajo del modal. Van en pestaña
aparte: se consultan y se vuelve con lo escrito intacto.
Y aparte, el carrusel de la portada. Cada lámina lleva su fondo con
`z-index: -1`, pero con `opacity: 1` la lámina no crea contexto de
apilamiento, así que ese fondo se escapaba y se pintaba detrás del
carrusel entero. Al pasar de lámina asomaba la foto de la de al lado y el
fondo de la que entraba saltaba hacia atrás al terminar el fundido.
Parecía que el shader se desincronizaba; lo que se movía era el orden de
pintado.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El ancho útil vuelve a 74rem (1184px). Se había subido a 88rem para
aprovechar los monitores anchos y salió al revés: la prosa tiene una
medida de lectura y no crece con el contenedor, así que lo único que
creció fue el hueco a su derecha.
Un vacío al lado de un texto no se tapa estirando el texto —probado: 129
caracteres por línea, ilegible— sino poniendo algo al lado:
- Los textos legales van a tres columnas: los otros documentos, el que se
lee y sus apartados. El índice de apartados sale del Markdown en bruto
y las anclas las pone un plugin al compilarlo, así que hay pruebas de
que las dos formas de calcularlas siguen coincidiendo.
- El índice legal pasa de lista de renglones a rejilla de tarjetas, como
el taller y los estilos; era el único listado que no se veía como los
demás.
- La bio lleva al lado lo que hay publicado —temas, discos y estilos, con
sus enlaces—, sacado del catálogo y no escrito a mano, para que no se
contradiga sola el día que se suba una canción.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cinco páginas legales en /legal, enlazadas desde el pie de todas las páginas:
información legal, condiciones de uso, privacidad, cookies y condiciones de
venta. Son borradores con los datos del titular marcados entre corchetes y un
aviso de que hace falta revisión de un abogado antes de vender: cubren lo que
pide la ley española, no son un documento validado.
La política de cookies y la de privacidad están escritas mirando el código, no
copiadas de una plantilla: dos cookies, las dos necesarias, y dos terceros
—Stripe y Resend— que reciben lo imprescindible.
Y eran tres, porque las tipografías venían de Google: cada visita mandaba la IP
del visitante a un tercero sin pedirlo y sin que aportara nada. Ahora se sirven
desde el propio dominio. Cargar la web ya no contacta con ningún dominio ajeno,
y hay una prueba que lo comprueba.
Aparte: el símbolo de marca registrada en el aviso de derechos, los botones de
reproducir de las tarjetas pasan de 48 a 36 píxeles —eran casi una quinta parte
de la portada, y cambiaban de proporción según la tarjeta— y el de ver el
carrito solo aparece cuando hay algo dentro.
De paso se arregla un fallo de tipos que estaba latente: `resolve()` tiene una
sobrecarga por ruta y no acepta una unión, así que la navegación dejaba de
compilar en cuanto se añadían rutas. Ahora se resuelve en `site.ts`, una a una.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El texto del botón dorado se aclaraba al pasar el puntero y quedaba en 1,67 de
contraste sobre el oro. La causa no era el color del botón: `.btn:hover` pone
`color: inherit` —para que la regla general de los enlaces no tiña de dorado los
secundarios— y le gana por especificidad. Ahora el dorado repite su color ahí.
De paso, el dorado se parte en dos tokens porque hacía dos trabajos
incompatibles: de relleno tiene que ser claro para que encima se lea el marino
—7,6 de contraste en el tema claro— y escrito tiene que ser hondo para leerse
sobre el fondo —5,3—. Con un solo valor, el tema claro se quedaba en 4,4, por
debajo del mínimo. Y encima del oro va marino, no blanco, en los dos temas.
Los botones tenían tres tamaños distintos: el dorado a 16px con relleno 8/16 y
el de al lado a 14px con 12/24. Dos botones juntos con distinta letra se ven
como un descuido. Ahora hay una sola escala, y los 15px son los del menú.
El botón de ver el carrito solo aparece cuando hay algo dentro, con la cuenta:
uno que lleva a una página vacía gasta el sitio de una llamada a la acción para
decir que no hay nada.
Tres pruebas nuevas lo fijan, incluida la del contraste con el puntero encima,
que es donde estaba el fallo y donde una captura en reposo no lo enseña.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Los botones dejan de ser cápsulas y las tarjetas pierden la mitad del canto. De
paso se ordena el sistema: había tres radios sueltos y una píldora de 999px
repetida en quince archivos, y quedan dos —uno para lo que se pulsa, otro para
las superficies—. Los botones de icono suelto siguen redondos, que ahí sí tiene
sentido.
Las fichas del taller no usaban la tarjeta compartida: tenían su propio borde,
su propio canto y su propio hover, así que se veían de otra familia que las de
música. Ahora usan `.tarjeta`, con el mismo reflejo bajo el puntero y el mismo
subrayado.
Y la portada arranca en el borde de la ventana, con la cabecera flotando encima.
Para eso la cabecera pasa a medir su propio alto y publicarlo: estaba escrito a
mano en 4,75rem cuando mide 68px, y todo lo que dependía de ese número —el salto
a un ancla, las barras laterales pegajosas— quedaba desplazado.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Lo escribí suponiendo, lo probé a mano contra la máquina y las suposiciones
fallaron en dos sitios:
- `rsync` no está en la máquina de desarrollo. Git Bash no lo trae. Se sube con
`tar` por SSH, que sí está en los dos lados.
- `npm ci` no funciona en el servidor. Tiene salida a internet, pero su DNS
resuelve unos dominios y otros no: `google.com` sí, `registry.npmjs.org` no.
Las dependencias se resuelven aquí y se suben instaladas. Los tres paquetes de
producción son JavaScript puro, así que viajan tal cual; el script comprueba
que sigan siéndolo y se para si aparece un binario de plataforma.
Comprobado en el servidor y no solo en el papel: la web arranca, las siete rutas
que probé devuelven 200, la muestra de audio sale entera y la ficha lee las
valoraciones de Postgres por el bucle local.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El servidor del usuario lleva Nginx, así que el montaje es proxy inverso por
delante y Node por detrás. La web no son archivos estáticos —cada página se
genera sabiendo quién la pide—, de modo que hace falta un proceso vivo y un
servicio que lo mantenga.
Nginx sirve además los estáticos por su cuenta: lo de `_app/immutable` lleva el
hash del contenido en el nombre y se cachea para siempre, así que abrir la web
deja de despertar a Node para devolver un CSS. Lo que no puede tocar son las
descargas: comprueban la compra antes de entregar el archivo, y el audio
completo vive fuera de `build/` para que no haya forma de pedirlo por URL.
El script construye aquí y sube el resultado, en vez de compilar en el servidor:
allí no hacen falta ni Vite ni el compilador, solo Node y tres paquetes. Las
migraciones también se lanzan desde aquí, porque `drizzle-kit` es herramienta de
desarrollo y en producción se instala con `--omit=dev`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El usuario ha puesto un servidor con PostgreSQL 16, así que la base pasa allí.
El catálogo no se toca: sigue siendo Markdown del repositorio; lo que se muda es
lo que generan las personas, que es lo único que estaba en la base.
Dos diferencias entre motores que habrían pasado desapercibidas y se cazaron
comprobando con datos de verdad, no compilando:
- `integer` en Postgres son cuatro bytes y en SQLite ocho. El limitador de
envíos guarda su vencimiento en milisegundos desde 1970 —trece dígitos, del
orden del billón— y en `integer` desbordaría, dejando de limitar. Va en
`bigint`.
- `avg()` y `count()` los entrega el driver como cadena para no perder
precisión. Sin convertirlas, `cuantas === 0` nunca se cumpliría y «1 voto»
saldría siempre en plural. Se convierten en la consulta.
Verificado contra el servidor: once tablas creadas, las 113 pruebas de extremo a
extremo y las 224 unitarias en verde, el servidor de producción arrancando, el
limitador escribiendo sus trece dígitos y el borrado en cascada llevándose
valoraciones y favoritos al borrar una cuenta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La página pasaba de las setecientas líneas y había que recorrerla entera para
tocar cualquier cosa. La columna de la derecha —valoración, ficha, créditos y
registro— son cuatro bloques de información que no tienen nada que ver con lo
que hace la página, que es presentar la letra y dejarla sonar.
Quedan 437 líneas en la página y 276 en el componente. De paso se va un bloque
de CSS muerto: un `@media` vacío con el comentario de las etiquetas, que se
quedó atrás cuando las pastillas pasaron a la clase global `.chip`.
Comprobado en el navegador, no solo compilando: los cuatro bloques salen en el
mismo orden, el de registro sigue apareciendo solo cuando hay algo que enseñar,
y no hay ningún error en consola.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El botón dorado se quedaba en un rectángulo oscuro con el borde de oro al pasar
el puntero. La causa: `.btn:hover` usaba el atajo `background`, que reinicia
también `background-image` y se llevaba el degradado por delante. Las reglas
base pasan a `background-color`, que es la longhand que no pisa lo que no le
toca.
De comprobarlo salió que no había ningún `a:hover` global: cualquier enlace sin
regla propia —el del feed, los del pie— no daba ninguna señal de ser pulsable.
Ahora todos responden con el dorado, que es el color de lo accionable aquí.
Tampoco la tenía el conmutador de vista de la página de música.
El botón secundario acusa el puntero con el fondo y el canto, no tiñendo el
texto de dorado: sería una señal doble y competiría con el botón dorado que
suele tener al lado.
Seis pruebas nuevas cubren la respuesta al puntero de cada tipo de control. Es
lo que faltaba: un estado de hover roto no se ve en una captura en reposo, y así
fue como se coló.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El pack pasa de cinco a diez canciones por el mismo 1,50 €: diez canciones por
lo que cuesta un café. Es la propuesta, así que el número y el importe se leen
juntos en todas partes.
Las pruebas de la tienda estaban escritas con el cinco metido a mano y hubo que
reescribirlas enteras. Ahora dicen la regla en términos del propio tamaño del
pack, y el helper de las pruebas de extremo a extremo añade lo que haya a la
venta hasta llenarlo en vez de nombrar los temas uno a uno: así cambiar el
tamaño otra vez no obliga a tocar nada.
Las dos láminas de llamada estrenan foto propia con su velo, más cubriente que
el de la textura: sobre una imagen clara el texto blanco no se lee.
Y el dorado deja de ser plano en los botones. Un metal refleja, y lo que lo hace
parecer metal es la banda de luz que lo cruza; el fondo se pinta más alto que el
botón para que al acercar el puntero la luz recorra la pieza en vez de
limitarse a aclararla. El acento sube un punto de croma y baja uno de claridad,
que es lo que lo separa de un amarillo de rotulador.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Un shader de ondas en los azules de marca, sobre la imagen de la portada. Es
WebGL a pelo: un fondo así es un triángulo y un programa de fragmentos, y
traerse una biblioteca 3D para eso sería medio mega de dependencia en la primera
pantalla del sitio.
El dibujo es domain warping —ruido que desplaza el dominio de otro ruido—, con
la escala horizontal mayor que la vertical para que las formas se tumben en
bandas en vez de quedarse redondas.
La paleta va en RGB lineal, que es en lo que mezcla el shader. El primer intento
usó los cuatro fondos de `app.css` tal cual y salió una plancha lisa: caben en
un pelo de claridad, y sin recorrido de color el ruido no se ve por bien que
esté calculado.
Con `prefers-reduced-motion` no se monta; se para al salir de pantalla y al
cambiar de pestaña; y si no hay WebGL o el programa no compila, debajo queda la
imagen de siempre. Solo lo lleva la cabecera: en las tres láminas serían tres
shaders pintando lo mismo a la vez.
De paso resuelve el tema claro, que no tenía imagen propia: lo pinta en azules
pálidos, así que el clip de relleno se va.
Las pruebas arrancan Chromium con SwiftShader para poder comprobar el efecto y
no solo su ausencia.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tres piezas estaban copiadas en varios sitios y ahora se escriben una vez: el
fondo a sangre de la portada, las migas de pan y la pastilla de una etiqueta.
El fondo ya había divergido entre la cabecera y las láminas de llamada: el
degradado era distinto por descuido y no por decisión, que es lo que pasa con el
marcado duplicado.
Las migas pasan a ser una lista ordenada, que es su marcado correcto: un lector
de pantalla anuncia cuántos escalones hay y en cuál se va, y el último deja de
ser un enlace a donde ya estás.
Tres cosas salieron de hacerlo, ninguna cosmética:
- Con el texto como clave del bucle, un disco y un tema que se llaman igual
—«Como si nada» es las dos cosas— daban claves duplicadas y tumbaban la ficha
al hidratar. El SSR salía bien, así que solo se veía en el navegador.
- Al compartir el fondo, las tres láminas montaban el clip de vídeo. En tema
claro eso eran tres copias del mismo vídeo en la portada. Ahora lo pide solo
la cabecera.
- La lista de temas de un disco no tenía nombre, así que no se distinguía de
ninguna otra lista de la página. Ahora se llama «Temas».
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El campo `volverA` de los formularios se comprobaba en tres sitios con tres
copias del mismo filtro, y las tres dejaban pasar `/\otro-dominio.com`: el
navegador convierte la barra invertida en barra normal al resolver, así que eso
es `//otro-dominio.com` con otra ropa. En la pantalla de entrar eso es phishing
servido: enlace a nuestro propio acceso, y quien escribe su correo acaba fuera.
Ahora hay un solo `destinoSeguro`, que no compara prefijos sino que resuelve la
URL y solo acepta lo que sigue en el origen. Con pruebas de cada disfraz.
Fuera siete funciones exportadas que no llamaba nadie. Las escribí "por si
acaso" y una API que no se usa no es una API: es código que leer y mantener sin
que nada dependa de ella.
Del repaso responsive salen dos fallos reales. Los puntos del carrusel medían
8×8 píxeles, la tercera parte del mínimo para acertarles con el dedo; ahora el
punto se ve igual y el botón mide 24×24. Y el hueco reservado bajo el contenido
para la barra de reproducción estaba escrito a mano en 88 píxeles cuando la
barra mide 97 en escritorio y 145 apilada en un móvil: se comía el pie. Ahora la
barra se mide sola y publica su alto.
Todo ello fijado en `responsive.e2e.ts`, que recorre diez páginas a cuatro
anchos.
Sobre rendimiento, nada que hacer: parecía que las páginas tardaban 215 ms, pero
eran de la resolución de `localhost` por IPv6 en la medición. Por IPv4 se
renderizan en 5-9 ms, así que memorizar el catálogo habría sido optimizar un
problema inexistente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La portada abre con tres láminas: el disco en el tocadiscos, una invitación al
taller y otra a la tienda. Un carrusel mal hecho es de las piezas más hostiles
que hay —cambia mientras lees, no se para, y para un lector de pantalla es un
cajón que aparece y desaparece—, así que este se para al pasar el puntero o al
entrar el foco, tiene botón de pausa, no gira solo con `prefers-reduced-motion`
y deja las láminas ocultas con `inert`, fuera del orden de tabulación.
Las letras que no están en español se enfrentan con su traducción en dos
columnas, verso a verso. Cada columna declara su `lang`, que es lo que impide
que un lector de pantalla lea el inglés con voz española, y la traducción se
pinta con `pre-line`: es verso, y perder los cortes la deja inservible para
seguir la original. El nombre del idioma sale de `Intl.DisplayNames`.
Sobre el reproductor: ya sobrevivía a la navegación, porque vive en el layout
raíz fuera de `main`. Lo que faltaba era comprobarlo de verdad; el test decía
que la barra seguía visible, no que siguiera sonando. Ahora mira que la posición
avance.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La palabra se va del proyecto entero, interfaz e identificadores. Venía del
encargo inicial pero no es la del oficio y se colaba en la interfaz: lo que hay
son estilos, discos y temas. Lo que agrupa un estilo con su obra pasa a ser
`EstiloConObra`, y el componente que lo pinta, `BloqueDeEstilo`.
La portada cuenta el catálogo en tres pasos: el estilo principal a fila
completa, sus discos debajo, y el resto en una rejilla donde cada tarjeta dice
cuánto hay y de qué se habla. Las etiquetas de un estilo no se declaran: salen
de sus temas, así que no hay dos sitios que mantener.
La página de un estilo estrena su propio `h1` —antes heredaba un `h2` del
listado, y se quedaba sin encabezado de nivel uno— y pierde el botón de
reproducir suelto en la cabecera, que no decía qué iba a sonar. Los demás
estilos la acompañan en una barra lateral que ahora vive en el layout de la
sección, así que la llevan también la portada de música y cualquier página que
se añada.
Las tarjetas se mueven de otra manera: reflejo que nace bajo el puntero, borde
dorado, portada que recupera el color, subrayado que crece y respuesta al
pulsar. Nada usa la curva por defecto.
Dos rarezas del minificador de CSS, ambas comprobadas en el build y ambas
dejaban efectos muertos: junta las funciones de un filtro sin el espacio que
las separa, y borra `backdrop-filter` sin prefijo si se declara también la
versión `-webkit-`.
Y la canción real estrena su vídeo de YouTube.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Se generan en la raíz al verificar un cambio en el navegador y no son parte del
proyecto. Se ignoran a partir de ahora para que no vuelva a pasar.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La portada estrena imagen de fondo propia, detrás del tocadiscos y del texto.
Llega en PNG de 2,1 MB y se sirve en tres anchos WebP: 10 kB en un móvil, 57 kB
en un panel grande. El velo que va encima se afloja hasta lo justo para que el
texto se lea, porque taparlo entero sería no haberlo puesto. El tema claro
todavía no tiene su versión, así que ahí sigue el clip de reserva.
La cabecera deja ver lo que pasa por debajo. La transparencia va con desenfoque
y no sola: sobre una imagen a sangre, un fondo solo translúcido dejaría el
contraste del menú a merced de lo que quedara detrás.
Y fuera las líneas entre secciones: separa el aire. Se quedan las que dibujan
una pieza —el pie, el reproductor, las filas de un listado, la tabla—.
Aparte, scripts/construir.mjs comprobaba `build/server/index.js`, un archivo
que adapter-node no genera: el servidor sale troceado en chunks. Daba por
fallido un build correcto.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>