La sección que saqué del build

Colecciono discos desde hace tiempo, y hace poco lo retomé porque me apetecía volver a comprar en vinilo. La lista la llevo en Discogs, una base de datos de música donde cada usuario puede registrar los discos que tiene, y la mantengo al día porque me sirve: la consulto cuando estoy a punto de comprar. Existía antes que esta parte del sitio y seguiría existiendo sin ella.
En algún momento me apeteció enseñarla aquí, y así apareció Música, una de las entradas del menú: la rejilla con la portada, el artista, el título, el formato y el año de cada disco, y una ficha por disco con sus canciones. De momento solo hay vinilos, pero la sección no tiene por qué limitarse a ellos.
Con esta se cierra la serie sobre cómo está hecho este sitio. Las tres partes anteriores cuentan lo que se construye en el build; esta, la sección que acabé sacando de él.
Enseñar esa lista aquí resultó ser un problema distinto de publicar un artículo, y la razón está en cómo está construido el resto del sitio.
Todo este sitio se construye una vez y se sirve como archivos ya escritos. Esta sección hace justo lo contrario, y a continuación explico por qué.
En Un blog que se publica solo conté que 11ty escribe el HTML de cada página durante el build y que GitHub Pages se limita a entregarlo. Eso funciona porque el contenido es mío: un artículo no cambia mientras nadie lo edite. La colección no cumple esa condición —se actualiza por su cuenta, cada vez que compro un disco y lo apunto—, y ahí es donde el planteamiento se rompe.
Los datos no son míos
Cuando escribí el plan de esta sección eran 224 discos. Ya son unos cuantos más, y para cuando leas esto seguramente más todavía. Ninguno de ellos ha necesitado que yo despliegue el sitio para aparecer.
Ese es todo el argumento. Un sitio estático puede pedirle los datos a una API en dos momentos, y elegir uno u otro no es una cuestión técnica.
En el build. La llamada la hace el runner, una vez por despliegue, y el visitante recibe el HTML con los discos ya escritos. Se sirve rápido, funciona sin JavaScript y aguanta aunque Discogs se caiga. Pero un disco nuevo no aparece hasta que yo vuelva a desplegar.
En el navegador. La llamada la hace cada visitante al abrir la página, que llega vacía y la rellena JavaScript. Un disco nuevo aparece en la siguiente visita, sin que yo toque nada.
Empecé por el build, y así estuvo publicada. Me pasé al navegador por esa última diferencia: con los discos metidos en el build, cada compra habría exigido un despliegue para que el sitio dijera la verdad. Un generador de sitios estáticos congela el contenido en el momento del build, y eso deja de ser una ventaja en cuanto el contenido lo edita alguien que no eres tú.
El despliegue se abarató de paso, y bastante: sacar los discos del build dejó de descargar una portada por disco en cada publicación, y la construcción pasó de casi tres minutos a menos de un segundo.
Los datos de la colección los mantengo en Discogs porque ahí es donde tiene sentido mantenerlos: hay una base de datos comunitaria con las fichas de las ediciones, y no voy a duplicarla en mi repositorio.
Descarté el navegador por un argumento falso
Esta es la parte incómoda y la razón de que este artículo exista.
La primera versión pedía los datos durante el build, desde el archivo que hacía las peticiones a la API de Discogs y dejaba los discos preparados para las plantillas. Ahí dentro yo mismo había escrito un comentario explicando por qué la colección no podía pedirse desde el navegador: serían 224 peticiones por visitante, una por disco, y acabarían fallando por el límite de peticiones de la API de Discogs.
No era cierto. El endpoint de la colección devuelve, junto a cada disco, un
objeto basic_information con el artista, el título, el año, el formato y la
portada. Es decir, con todo lo que la rejilla pinta. Con cien discos por página
son tres peticiones, las haga el runner o las haga un visitante. Las 224
solo harían falta pidiendo la ficha completa de cada disco, que no se hace en
ningún momento.
Y el límite tampoco funcionaba como yo creía: Discogs cuenta 25 peticiones por minuto por IP de origen, así que cada visitante trae su propio cupo y gasta 3 de 25. No hay un contador compartido que se agote entre todos.
Y la prueba de que mi comentario mentía estaba unas líneas por debajo de él: ese
basic_information le pasaba por delante en cada build.
Nunca lo comprobé porque lo había escrito yo, sonaba seguro y estaba en el sitio donde uno espera encontrar la explicación de lo que hace el código. Cada vez que pasaba por ahí lo leía como un hecho, cuando era una suposición de la primera versión.
Nota mental: si un comentario justifica una decisión, le escribo dentro el dato en el que se apoya. Aquel daba la conclusión —desde el navegador no se puede— y se guardaba el motivo, y para dudar de una conclusión sin motivo hay que rehacer el razonamiento entero.
Lo que hay que comprobar antes de llamar a una API desde el navegador
Llamar a una API ajena desde el cliente depende de cosas que decide la API, no
tú. Estas las comprobé una a una contra Discogs, con curl:
curl -s -D - -H "Origin: https://jestemrique.github.io" \
"https://api.discogs.com/users/Nrique/collection/folders/0/releases?per_page=100&page=1"
- CORS abierto.
La respuesta trae
access-control-allow-origin: *. Sin esa cabecera el navegador se niega a entregarle la respuesta a mi JavaScript y no hay nada que hacer desde mi lado: no es un permiso que se pida, es uno que se concede. - Sin credencial. La colección es pública y responde 200 sin token, igual que la ficha de cada disco.
- El
User-Agentes obligatorio para clientes que no son navegadores. Desde el navegador no hay nada que hacer, y es a favor: JavaScript tiene prohibido tocar esa cabecera, y el navegador manda la suya. - Discogs no manda ninguna cabecera de caché: ni
Cache-Control, niExpires, niETag. Ante una respuesta sin instrucciones el navegador aplica caché heurística, o sea que decide él cuánto tiempo la da por buena. Como lo único que esta sección promete es estar al día, los dosfetchvan concache: "no-store". - No hay webhooks. Lo busqué porque habría sido la alternativa elegante: Discogs avisa, yo despliego. No está implementado. La única forma automática de enterarse desde el build habría sido preguntar cada cierto tiempo.
Lo de la caché es el que más fácil se pasa por alto, porque no falla: sin
no-store la sección seguiría funcionando y a veces enseñaría discos de hace
unas horas, que es exactamente el problema que fui a resolver.
Lo que se paga
No es gratis. Pedir los datos desde el navegador tiene sus costes: unos los vi venir y otros me los encontré por el camino. Estos son los que conozco.
Sin JavaScript la página está vacía. Con ella se quedan fuera los rastreadores que no lo ejecutan y las vistas previas al compartir el enlace, que leen el HTML tal como llega. Hay una espera en cada carga, corta pero real. Y Discogs pasa a ser un punto único de fallo de esa sección: si su API no responde, ahí no hay nada.
Hay además un límite que no se ve hasta que se choca con él. Un sitio estático no tiene secretos: todo lo que necesita el navegador se lo puede leer el visitante. Aquí no importó porque mi colección es pública, pero el día que hiciera falta un token, ese token solo podría vivir en el build. Ponerlo en el cliente es publicarlo. Esta arquitectura y una API con credenciales no encajan.
Si algún día la colección deja de ser pública, esto hay que rehacerlo, y solo se me ocurren dos salidas. Una es volver al build, guardando el token como secreto de GitHub Actions —que es donde ya vive la clave del despliegue— y aceptando otra vez que un disco nuevo espere a la siguiente publicación. La otra es poner algo en medio que guarde el token y hable con Discogs por mí, dejando el sitio estático salvo por ese punto, a cambio de una pieza más que mantener. No lo he decidido porque todavía no me hace falta.
Entre la API y la página siempre hay una capa de traducción
Esto no cambió al mudarse al navegador: es el mismo código, en otro archivo. Y es donde está casi todo el trabajo de la sección, porque los datos que llegan no tienen la forma que uno querría pintar.
Los nombres traen un número detrás. Discogs devuelve Jeanette (6) o
Lolita (5), porque dentro de su base de datos ese número distingue a los
artistas que se llaman igual. Fuera de Discogs parece una errata, así que lo
quito.
El orden alfabético literal no sirve. Ordenando por la primera letra se apilan un montón de discos bajo la T de The Cure y la L de La Casa Azul. Ordeno ignorando el artículo, como en una tienda de discos, pero enseño el nombre completo.
Y las tildes tampoco ordenan solas. El orden por defecto de JavaScript manda
los acentos detrás de la Z, así que Ángel iría después de Zappa. Se arregla
comparando por idioma con localeCompare.
El formato viene anidado y en plural. Un disco puede tener varios soportes a
la vez —una caja de vinilos es Vinyl y Box Set—, así que hay que elegir cuál
mandar: si hay vinilo, es un vinilo.
Y a veces falta el año. Algunos discos no lo traen. No pinto nada: ni un guion ni un «desconocido», que ocupan sitio para decir que no hay dato.
El año que falta no lo puedo arreglar. No es un campo mío: pertenece a la ficha de la edición en la base de datos comunitaria, no a mi copia del disco. Se programa contra los datos que hay.
Los textos tampoco viven en el JavaScript. Viajan en un
<script type="application/json"> que escribe la plantilla, y de ahí salen los
soportes ya traducidos desde un
archivo de datos de 11ty. Así el módulo es
idéntico para las dos versiones del sitio. Es la misma idea que sostiene la
parte 3: la clave que guarda Discogs
es neutra, y cada idioma le cuelga su etiqueta.
Una sola página para todas las fichas
Cada disco tiene su ficha, y ninguna existe como archivo.
La aritmética lo decidió. Generar las fichas en el build es una llamada por disco, y a 25 peticiones por minuto eso son varios minutos de despliegue que además crecen con cada disco que compro. Pidiéndola al abrirla es una llamada, y solo del disco que alguien mira.
Lo que sí quería eran direcciones con forma de dirección:
/es/musica/disco/10000-maniacs-blind-mans-zoo-591270/
/en/music/record/10000-maniacs-blind-mans-zoo-591270/
Esa ruta no existe. GitHub Pages sirve 404.html para cualquier ruta que no
encuentre, así que ese archivo hace dos trabajos: es el 404 real del sitio —que
antes no tenía— y reconoce las rutas de ficha y las reenvía a la página que sabe
pintarlas. Después el JavaScript devuelve la dirección bonita a la barra.
El coste es que la respuesta lleva estado 404 aunque la ficha se vea perfectamente. Un visitante no lo nota; un rastreador sí.
Y sí, es otra de las cosas que tengo pendientes de resolver. Funciona, pero es un truco: el servidor dice que esa página no existe mientras la página enseña un disco. Como esa página ya la pintaba JavaScript y no era rastreable, en la práctica no perdí nada que tuviera. Y si algún día genero las fichas en el build, estas direcciones no cambian: empiezan a existir y devuelven 200.
El número del final tampoco es adorno. Sobre los 224 que había al medirlo, 221 slugs eran únicos: Annabel Lamb — Once Bitten aparece dos veces, las dos ediciones de 1983, y La Pandilla tres. Cinco discos se quedarían sin dirección propia. Hay un segundo motivo que no se puede medir: los títulos los edita la comunidad, así que una corrección ajena rompería en silencio cualquier enlace ya compartido. El slug es texto para quien lee; el que identifica es el número.
Al volver de una ficha a la colección, el navegador vuelve a ejecutar el módulo
y pedía los discos otra vez. Ahora la colección se guarda en
sessionStorage:
se pinta lo que hay para que la vuelta sea inmediata y mientras tanto se piden
los datos frescos, repintando solo si han cambiado. Es sessionStorage y no
localStorage a propósito, porque se borra al cerrar la pestaña: guardar mi
colección para siempre en el disco de un visitante sería quedarme con algo que
nadie me ha dado.
El ancho de pantalla miente
La ficha de un disco son dos columnas —portada a un lado, datos al otro— y no hay ninguna media query que las decida.
Cuando fui a escribir el umbral, medí el espacio del que dispone de verdad esa página a distintos anchos de ventana:
| Ventana | 390 | 600 | 768 | 1024 | 1280 |
|---|---|---|---|---|---|
| Sitio útil | 305 | 459 | 350 | 595 | 790 |
A 768 hay menos sitio que a 600. No es un error de medida: a partir de ahí
aparece la barra lateral de navegación, que en pantallas pequeñas es una barra
inferior y no roba anchura. Un min-width: 768px habría metido dos columnas
justo donde peor caben.
La ventana no es el contenedor. Lo que manda aquí es el espacio que le queda a esta página después de todo lo demás, y para eso están las consultas de contenedor:
@container (min-width: 34rem) {
/* dos columnas */
}
Debajo había otro fallo que tardé más en ver, porque no era de anchura. Los títulos los escribe Discogs, y los hay de 104 caracteres; la escala tipográfica del sitio le daba al título de la ficha 48,8px, que es el tamaño de un titular de artículo. El Sonido Efervescente de La Casa Azul ocupaba ocho líneas y 439px, y dejaba la columna de texto 231px más alta que la portada. Bajando el cuerpo a 1.75rem son cuatro líneas, y el desajuste entre columnas pasó de un rango de 351px a uno de 88.
Las cosas que se rompieron
Un @container en rem no se rompe, se apaga
La ficha estuvo en una sola columna varias semanas sin que nadie lo notara.
El umbral no se movió; se movió el ancho que tiene debajo. Un cambio del rediseño —que no tocó ni una línea de esta página— dejó el ancho útil en 624px, y el umbral de entonces, 38rem, son 646. La página se quedaba 22 píxeles por debajo de su propia regla.
Un umbral que no se cumple no da error ni aviso. La regla simplemente no se
aplica y el diseño se queda en su estado anterior, que además es un estado
válido: una columna se ve perfectamente, solo que no es lo que había decidido.
Lo encontré mirando la página, no una prueba. Ahora está en 34rem, que deja
46px de holgura, y sigue en rem a propósito, porque quien agranda el texto del
navegador necesita que la ficha vuelva a una columna antes que quien no lo hace.
El índice abrió un agujero y el buscador es quien lo tapa
La colección tiene un índice alfabético: pulsas una letra y se queda solo esa.
Las secciones que no tocan se ocultan con el atributo
hidden,
que las retira del árbol de accesibilidad, como debe ser.
También las retira del buscador del navegador. Con una letra abierta, Ctrl+F
solo encuentra dentro de esa letra, y quien no se haya fijado en que hay un
filtro activo concluirá que el disco no está.
Por eso el buscador de la página busca siempre en la colección entera aunque haya una letra abierta, y escribir en él deshace el filtro del índice. Si respetara la letra seleccionada dejaría el mismo agujero abierto, solo que con otra interfaz.
Pico calcula en vez de heredar
Dos veces, en el mismo trabajo.
El campo de búsqueda se quedaba en 51px de alto y no bajaba tocando el interlineado. Pico no le deja el alto al contenido: se lo calcula con una fórmula que suma el interlineado, el relleno vertical y el borde. Cambiar el interlineado movía el texto dentro de la caja sin mover la caja.
Y la rejilla de discos llevaba viñetas puestas desde el principio sin que se
vieran. Pico declara list-style: square sobre el li, no sobre el ul, así
que quitarlo desde el ul no sirve por mucha prioridad que se le dé. En la
parte 2 conté cómo las capas de
cascada resuelven quién gana cuando dos reglas apuntan al mismo elemento. Aquí
las capas no sirven, porque el problema no es quién gana: es que las dos reglas
apuntan a elementos distintos. Las viñetas se dibujaban en el
hueco entre celdas y pasaban por suciedad del fondo hasta que un marco nuevo las
delató.
El primer clic en una letra saltaba al fondo de la página
Un enlace a un ancla hace dos cosas, en este orden: salta al destino y después avisa de que el fragmento de la dirección ha cambiado. Así que el navegador saltaba con la página entera desplegada, y solo entonces mi código la encogía a una letra. El destino dejaba de existir a mitad del salto.
Medido a 1280 pasaba por la posición 12.214 de una página de 22.272 y acababa en la 1.041, que es el fondo de la página ya filtrada. Solo se veía en el primer clic, porque a partir de ahí ya estabas al fondo. Se arregla haciendo las cosas en el orden contrario: interceptar el enlace, filtrar y colocar el desplazamiento al final.
Lo que aprendí
La pregunta con la que empecé esta sección era cómo meter mi colección en el build. Era la pregunta equivocada, y me costó una reescritura entera darme cuenta de que la respuesta correcta era no meterla.
Un generador de sitios estáticos hace una cosa muy concreta: coge el contenido en un instante, escribe el resultado y lo deja quieto. Para un artículo es exactamente lo que quiero, porque el instante en que lo escribo y el instante en que se publica son el mismo a todos los efectos. Para unos datos que cambian sin que yo toque el repositorio, congelarlos significa que el sitio dice la verdad el día del despliegue y va mintiendo un poco más cada día.
Lo otro que me llevo no es sobre arquitectura. Durante mucho tiempo tuve escrita en el código la razón por la que la colección no se podía pedir desde el navegador, y era falsa. No la comprobé antes porque estaba dentro del archivo que hacía el trabajo, escrita por mí, que es donde uno espera encontrar la verdad. La primera versión de este sitio la desplegué convencido de que un 200 significaba que funcionaba, y el fallo es el mismo: un comentario tampoco es una comprobación.