O bug da imagem de destaque que na verdade eram três bugs
Encontrei este durante teste manual das imagens de destaque self-hosted. Um post, “Self-hosting Backlogia, and fixing it before running it”, tinha uma imagem de destaque ativa no blog de verdade, mas a UI do blog-manager mostrava só uma caixa de busca vazia, como se o post não tivesse hero nenhum. Abri o chamado achando que seria um ajuste de uma linha. Não foi.
O que eu estava tentando fazer
O card de atribuição de hero do blog-manager e a página de detalhe do post tinham o mesmo bug: os dois checavam post.hero_image_url, a escolha em estágio, definida apenas pelo próprio seletor de Pexels/Unsplash do blog-manager, para decidir entre mostrar uma imagem ou uma caixa de busca. Nenhum dos dois checava post.live_hero_image_url, que é o estado canônico de verdade (definido por uma varredura do repositório, ou pelo job de commit logo depois que ele escreve no frontmatter do artigo publicado). Para qualquer post cujo hero foi definido de outra forma, um script irmão no vdaluz.com, ou uma imagem self-hosted que foi capturada na varredura, hero_image_url fica nil para sempre, e o card mente dizendo que o post não tem hero.
<% if post.hero_image_url.present? %>
<img src="<%= post.hero_image_url %>" ...>
<% else %>
<%# search box %>
<% end %>
O ajuste parecia óbvio: ampliar a condição para checar também live_hero_image_url. Não foi tão simples assim.
O que eu construí
Primeiro obstáculo: live_hero_image_url nem sempre é uma URL absoluta. Para imagens self-hosted, é um caminho relativo ao site, tipo /assets/images/slug.jpeg, exatamente o que o post de exemplo do relato de bug tinha. Trocar a condição de forma ingênua teria renderizado <img src="/assets/images/slug.jpeg">, que o navegador resolve contra a própria origem do blog-manager, não a do blog. Imagem quebrada, exatamente no cenário que o relato de bug descrevia.
Então adicionei um método no model para resolver isso direito:
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
Esse rescue não estava na minha primeira versão. Mais sobre isso adiante.
Decisões que tomei e por quê
Não mexi no botão “Choose a different image”. Uma vez que o hero de um post está ativo de verdade (commitado no frontmatter), o caminho de escrita é só de inserção por design, ele recusa sobrescrever uma chave heroImage já existente. Então, mesmo antes deste ajuste, escolher uma nova imagem num post já ativo já era um beco sem saída, o commit simplesmente falharia. Ampliar a condição de exibição torna esse botão alcançável em mais um estado (nada em estágio, hero já ativo), onde clicar nele agora é um no-op silencioso em vez de ficar completamente escondido. Deixei como estava em vez de tentar corrigir uma limitação que este chamado não pedia para eu corrigir.
Não fiz backfill da atribuição para heroes definidos fora do fluxo normal. O scanner do repositório lê a chave heroImage do frontmatter ativo para live_hero_image_url, mas nunca traz a informação de crédito (heroImageCredit) de volta para o banco. Então um hero definido pelo script externo agora exibe corretamente, mas sem a linha “Photo by…”. O partial de atribuição já degrada bem quando esses campos estão vazios, então isso não foi uma regressão, só uma lacuna que percebi e deixei como estava.
O que me surpreendeu
A parte que eu não vi vindo: verifiquei o ajuste localmente, o screenshot parecia certo, e aí, testando num navegador de verdade contra a instância de produção, apareceu um ícone de imagem quebrada. Descobri que o vdaluz.com protege seus próprios caminhos de assets contra hotlink pelo header Referer, e não só contra origens aleatórias. Testei contra o hostname de produção de verdade do blog-manager e também tomou 403. A instância de produção do meu próprio app não consegue fazer hotlink das imagens do meu próprio blog sem falsificar o Referer.
curl sem header Referer: 200. curl com qualquer Referer que não seja o próprio vdaluz.com: 403. Navegadores sempre mandam um Referer para uma <img>, a menos que sejam instruídos a não mandar, então o ajuste precisou de mais um atributo:
<img src="<%= post.hero_image_display_url %>" referrerpolicy="no-referrer" ...>
A outra surpresa veio de rodar uma revisão automatizada com múltiplos ângulos antes do merge, oito ângulos de revisão em paralelo, um dos quais encontrou e confirmou empiricamente, de forma independente, que minha chamada URI.join podia levantar exceção e derrubar a página inteira com 500. O campo base_url do blog é validado com URI.regexp, o que parece garantir uma URL limpa, mas essa regex não é ancorada, então ela só precisa dar match em algum lugar da string. "https://vdaluz.com/blog site" (com um espaço perdido) passa direto pela validação e depois explode o URI.join. Eu nunca teria pego isso manualmente, foi preciso um agente de revisão literalmente rodar URI.join contra strings de caso extremo num console Rails para provar. Adicionei o rescue, adicionei um teste de regressão para isso, e publiquei tudo no mesmo PR.
O que vem a seguir
Nada urgente, o ajuste está mergeado, testado, e verificado contra dados reais. Duas coisas que anotei para depois em vez de fazer agora: o gate de publicação do Dev.to ainda checa o campo errado (atualmente inofensivo, já que a publicação nativa no Dev.to está desabilitada até outro chamado ser resolvido), e a pegadinha da proteção contra hotlink só tem um ajuste do lado do cliente, qualquer coisa que busque essas imagens do lado do servidor (um enclosure de RSS, um payload de sindicação) ainda tomaria 403 e precisa de outra solução.
Leitura relacionada
Adicionando o Unsplash como um segundo provedor de imagem de destaque
Espelhando o client do Pexels linha por linha, um ping de rastreamento dispara-e-esquece, e uma URL do navegador que quase acabou com a chave de API anexada.
Construindo imagens de destaque auto-hospedadas antes de eu ter qualquer coisa que precisasse delas
Um pipeline de download e re-hospedagem para imagens CC: vips de um buffer, um force-encode UTF-8 que corromperia o JPEG, e uma fila velha parecendo bug.
O que acontece quando um job faz broadcast para ninguém
Fechando o loop da imagem de destaque: frontmatter só por inserção, uma guarda que pegou divergência real na primeira execução, e um broadcast sem ouvinte.
Você também pode achar útil
Proton Pass
Gerenciador de senhas focado em privacidade, da equipe por trás do Proton Mail.
Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).
Saiba maisProton Drive
Armazenamento em nuvem criptografado, da equipe por trás do Proton Mail.
Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).
Saiba maisProton VPN
VPN comercial com filtragem NetShield e interruptor de desligamento automático.
Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).
Saiba mais