O que acontece quando um job faz broadcast para ninguém
Esse issue devia fechar um loop que estava aberto no blog-manager havia um tempo: o seletor de imagens do Pexels deixa você deixar uma imagem de destaque em staging num post, mas nada nunca empurrava essa imagem, ou a atribuição obrigatória dela, para o post de blog realmente publicado. Você escolhia uma foto, o app lembrava dela, e depois… nada. Um comentário no código dizia literalmente “Applied to the live article by the direct-repo backfill” ao lado de uma funcionalidade que ainda não existia.
O que eu estava tentando fazer
Duas coisas, em dois repositórios diferentes. Primeiro, o vdaluz.com precisava de um lugar para colocar os dados de atribuição no frontmatter e uma forma de renderizá-los, “Photo by X on Pexels” embaixo da imagem de destaque. Segundo, o blog-manager precisava realmente escrever esses dados no arquivo markdown do post publicado no GitHub, usando a capacidade de escrita do issue anterior.
O que eu construí
No lado do vdaluz.com: um campo opcional heroImageCredit no schema de conteúdo, e um bloco de render que mostra a linha de crédito dentro do mesmo wrapper de conteúdo que a imagem de destaque. Esse posicionamento não é arbitrário, o importador de histórias do Medium só pega imagens de dentro da região de texto extraída, então qualquer coisa fora dela some silenciosamente quando um post é importado para o Medium. A linha de crédito aproveita essa mesma restrição de graça: já que renderiza no mesmo bloco, o importador do Medium a pega automaticamente quando faz o scrape da página publicada. Nenhum código necessário do lado do Medium.
O Dev.to é o caso oposto. Ele constrói o corpo do post a partir do arquivo markdown bruto, não da página renderizada, então uma linha de crédito que só existe como HTML renderizado por template nunca chega até lá. Esse precisou de uma correção explícita, colocando uma linha de crédito formatada em markdown no início do corpo antes de ele sair.
No lado do blog-manager: um service que faz patch no frontmatter do arquivo publicado, só inserção, nunca tocando no conteúdo existente, mais um job em segundo plano que o executa, espelhando o mesmo padrão de retry/descarte dos jobs de publicação no Dev.to e no Medium do app.
Decisões que tomei e por quê
Só inserção, não um reescritor geral. Eu poderia ter escrito algo que faz parse do bloco de frontmatter inteiro, mescla novas chaves, e reserializa tudo. Não fiz isso, porque um round-trip completo de YAML arrisca reformatar silenciosamente todo campo existente, reordenando chaves, mudando estilos de aspas, o que aparece como ruído indesejado no diff de todo post que toca. Já que todo post em que isso roda vem de uma fila de “hero faltando” (o que significa que o arquivo comprovadamente ainda não tem uma chave heroImage), a atitude segura era só sempre anexar as duas chaves novas e deixar toda linha existente intocada, byte a byte. Um editor de frontmatter sem perdas de verdade é explicitamente um trabalho separado, para depois, eu não queria construir sem querer uma versão pior dele como efeito colateral desse issue.
Uma guarda que checa o arquivo, não o banco de dados. Antes de escrever, o service checa de novo se o arquivo publicado já tem uma chave heroImage, usando o que acabou de buscar do GitHub, não o que o banco de dados acha. Essa distinção acabou importando quase imediatamente (veja abaixo).
O que me surpreendeu
Eu não tenho um blog de teste ou rascunho nesse app, só os dois de verdade. Então a única forma de verificar manualmente o caminho de escrita era apontar para o repositório real do vdaluz.com, usando o token real do GitHub (mas só leitura, por enquanto), com a expectativa de que falharia previsivelmente na etapa de escrita e eu poderia ver o tratamento de falha funcionando corretamente.
Falhou, mas não do jeito que eu esperava. Em vez de um erro de permissão, bateu na guarda de “esse arquivo já tem uma imagem de destaque”. O banco de dados dizia que esse post ainda não tinha imagem de destaque. O arquivo publicado de verdade tinha. Em algum ponto do caminho, os dois tinham desviado e saído de sincronia, provavelmente um post que ganhou uma imagem de destaque adicionada manualmente, fora da ferramenta, antes que um rescan chegasse a notar. A própria guarda que eu tinha escrito especificamente para desconfiar de estado obsoleto no banco de dados pegou uma instância real desse estado obsoleto logo no primeiro post publicado em que testei, e se recusou a tocar no arquivo. Nada foi corrompido. Isso pareceu um bom sinal de que a cautela valeu o código extra.
A outra surpresa veio da revisão, não de rodar o código. Uma passada automatizada que rodei antes de fazer merge sinalizou que a página em torno da qual essa funcionalidade inteira é construída, uma view em lote para passar por posts sem imagem de destaque, nunca de fato assina atualizações do job em segundo plano. O botão dispararia, o job rodaria, o commit teria sucesso contra o GitHub, e a única página que você realmente usaria para fazer esse trabalho em lote ficaria… parada, sem mudança, até você recarregar manualmente. O mecanismo de broadcast do Turbo não dá erro quando não há listener, ele só some silenciosamente sem ir a lugar nenhum. A página de detalhe do post tinha a assinatura; a página em lote não tinha. O mesmo partial, dois lugares diferentes onde é renderizado, só um deles tinha sido conectado para escutar. Fácil de passar batido, já que o clique em si ainda “funcionava”, só não era a versão de funcionar que mostrava alguma coisa para você.
O que vem a seguir
Quem pegar as imagens de destaque self-hosted ou os provedores Unsplash/Openverse vai poder construir em cima disso em vez de começar de um estado “em staging mas nunca commitado”. A única coisa que ainda fica totalmente fora do app: os tokens do GitHub dos dois blogs continuam só leitura. Escrever qualquer coisa de verdade significa entrar na UI do GitHub e reconfigurar o escopo de um token à mão, não existe API para essa parte.
Leitura relacionada
Uma correção de desvio de documentação que não foi tão chata quanto parecia
Três itens da auditoria que cada um virou outra coisa: uma alegação meio corrigida, uma recuperação de senha silenciosamente morta, e um e-mail de staging que apontaria para produção.
Um 500 escondido dentro das rotas isoladas de uma engine montada
O painel de jobs retornava um 500 em vez de uma página de login: helpers de rota sem qualificação resolvem contra a engine, não contra a aplicação. Uma linha, mais o gêmeo dormente dela.
Apagando código morto, e pegando um motivo errado para uma resposta certa
Uma issue de limpeza com uma justificativa errada, uma varredura de documentação que não era necessária, e a credencial órfã que uma revisão pegou.
Você também pode achar útil
RackNerd VPS
Hospedagem VPS econômica para serviços leves que funcionam continuamente.
Como afiliado da RackNerd, ganho com compras qualificadas.
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 maisProton Mail
E-mail criptografado de ponta a ponta, com arquitetura de acesso zero.
Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).
Saiba mais