El error de la imagen hero que en realidad eran tres errores
Encontré esto durante pruebas manuales de imágenes hero autoalojadas. Un post, “Autoalojar Backlogia, y arreglarlo antes de correrlo”, tenía una imagen hero activa en el blog real, pero la interfaz de blog-manager mostraba un simple cuadro de búsqueda, como si el post no tuviera ninguna imagen hero. Lo reporté pensando que sería un arreglo de una línea. No lo fue.
Qué estaba tratando de hacer
Tanto la tarjeta de asignación de hero de blog-manager como la página de detalle del post tenían el mismo error: verificaban post.hero_image_url, la elección en staging, que solo se define desde el propio selector de Pexels/Unsplash de blog-manager, para decidir si mostrar una imagen o un cuadro de búsqueda. Nunca verificaban post.live_hero_image_url, que es el estado canónico real (definido por un escaneo del repo, o por el job de commit justo después de escribirlo en el frontmatter del artículo en vivo). Para cualquier post cuyo hero se hubiera definido de otra forma, un script hermano en vdaluz.com, o una imagen autoalojada que se escaneó, hero_image_url se queda en nil para siempre, y la tarjeta miente diciendo que el post no tiene hero.
<% if post.hero_image_url.present? %>
<img src="<%= post.hero_image_url %>" ...>
<% else %>
<%# search box %>
<% end %>
El arreglo parecía obvio: ampliar la condición para verificar también live_hero_image_url. No fue tan simple.
Lo que construí
Primer tropiezo: live_hero_image_url no siempre es una URL absoluta. Para imágenes autoalojadas es una ruta relativa al sitio, como /assets/images/slug.jpeg, exactamente lo que tenía el post de ejemplo del reporte de bug. Cambiar la condición ingenuamente habría renderizado <img src="/assets/images/slug.jpeg">, que el navegador resuelve contra el propio origen de blog-manager, no el del blog. Imagen rota, justo para el escenario del que trataba el reporte de bug.
Entonces agregué un método al modelo para resolverlo correctamente:
def hero_image_display_url
return hero_image_url if hero_image_url.present?
return nil if live_hero_image_url.blank?
return live_hero_image_url if live_hero_image_url.match?(%r{\Ahttps?://}) || blog.base_url.blank?
begin
URI.join(blog.base_url, live_hero_image_url).to_s
rescue URI::InvalidURIError, URI::BadURIError
live_hero_image_url
end
end
Ese rescue no estaba en mi primer borrador. Más sobre eso más abajo.
Decisiones que tomé y por qué
No toqué el botón “Choose a different image”. Una vez que el hero de un post está en vivo de verdad (confirmado en el frontmatter), la ruta de escritura es de solo inserción por diseño, se niega a sobrescribir una clave heroImage existente. Así que, incluso antes de este arreglo, volver a elegir una imagen en un post ya en vivo ya era un callejón sin salida; confirmarlo simplemente fallaría. Ampliar la condición de visualización hace que ese botón sea alcanzable en un estado más (nada en staging, hero ya en vivo), donde hacer clic ahora es un no-op silencioso en vez de estar oculto por completo. Lo dejé como estaba en vez de intentar arreglar una limitación que este issue no me pedía arreglar.
No completé retroactivamente la atribución para heroes fuera de banda. El escáner del repo lee la clave heroImage del frontmatter en vivo hacia live_hero_image_url, pero nunca lee la información de crédito (heroImageCredit) de vuelta a la base de datos. Así que un hero definido por el script externo ahora se muestra correctamente, pero sin la línea “Photo by…”. El partial de atribución ya se degrada con elegancia cuando esos campos están vacíos, así que esto no fue una regresión, solo un vacío que noté y dejé como estaba.
Lo que me sorprendió
La parte que no vi venir: verifiqué el arreglo localmente, la captura se veía bien, y después probarlo en un navegador real contra la instancia de producción dio un ícono de imagen rota. Resultó que vdaluz.com protege sus propias rutas de assets contra hotlinking mediante el encabezado Referer, y no solo contra orígenes aleatorios. Lo probé contra el hostname real de producción de blog-manager y también dio 403. La instancia de producción de mi propia app no puede hacer hotlink a las imágenes de mi propio blog sin falsificar su Referer.
curl sin encabezado Referer: 200. curl con cualquier Referer que no sea el propio vdaluz.com: 403. Los navegadores siempre envían un Referer para una <img> a menos que se les indique lo contrario, así que el arreglo necesitó un atributo más:
<img src="<%= post.hero_image_display_url %>" referrerpolicy="no-referrer" ...>
La otra sorpresa salió de correr una revisión automatizada de múltiples ángulos antes de mergear, ocho ángulos de revisión en paralelo, uno de los cuales encontró y confirmó empíricamente, de forma independiente, que mi llamada a URI.join podía lanzar una excepción y tirar un 500 en toda la página. El campo base_url del blog se valida con URI.regexp, que suena como si garantizara una URL limpia, pero esa expresión regular no está anclada, así que solo necesita coincidir en algún lugar de la cadena. "https://vdaluz.com/blog site" (con un espacio de más) pasa la validación sin problema y después revienta URI.join. Nunca lo habría detectado manualmente; hizo falta que un agente de revisión corriera literalmente URI.join contra strings de casos límite en una consola de Rails para probarlo. Agregué el rescue, agregué una prueba de regresión para eso, y lo envié en el mismo PR.
Qué sigue
Nada urgente, el arreglo está mergeado, probado, y verificado contra datos reales. Dos cosas que anoté para después en vez de hacerlas ahora: el gate de publicación de Dev.to todavía verifica el campo equivocado (por ahora inofensivo, ya que la publicación nativa a Dev.to está deshabilitada mientras se resuelve otro issue), y la trampa de protección contra hotlinking solo tiene un arreglo del lado del cliente, cualquier cosa que traiga estas imágenes del lado del servidor (un enclosure de RSS, un payload de sindicación) seguiría dando 403 y necesita una respuesta distinta.
Lecturas relacionadas
Agregando Openverse: el tercer proveedor de imágenes, y el primero que nadie tiene que configurar
Más de 800 millones de imágenes CC y de dominio público, anónimo por defecto, y la consecuencia de diseño de que una instalación sin ningún proveedor configurado ahora hace llamadas reales a la API.
Agregar Unsplash como segundo proveedor de imágenes hero
Replicando el cliente de Pexels línea por línea, un ping de seguimiento de descargas fire-and-forget, y una URL controlada por el navegador que casi terminó con la clave de la API pegada a ella.
Construir imágenes de portada autoalojadas antes de tener algo que las necesitara
Un pipeline de descarga y realojamiento para imágenes con licencia CC: vips desde un buffer, una conversión forzada a UTF-8 que habría corrompido los bytes del JPEG, y una cola desactualizada que hizo parecer un error a una protección que funcionaba bien.