Apagando uma integração ao migrar ela para o Postiz
O blog-manager cresceu com uma personalidade dividida. Os cross-posts de artigo completo para o Dev.to e o Medium tinham cada um seu próprio cliente de API, fluxo de rascunho-depois-publica, e job de sincronização. Tudo que era social, Bluesky, Mastodon, X, LinkedIn, passava pelo Postiz, um agendador auto-hospedado. A linha entre “nativo” e “Postiz” não era uma decisão de design. Era só a ordem em que eu construí as coisas.
Quando eu arranquei o Hashnode recentemente, a duplicação ficou difícil de ignorar. Duas das minhas integrações eram ~80% o mesmo boilerplate: um cliente, um criador de rascunho, um job de sincronização, colunas de status por plataforma. Então rodei um spike com uma pergunta direta: o Postiz realmente publica artigos completos com URLs canônicas, ou só textos curtos de redes sociais? Se ele consegue publicar artigos, meu código nativo do Dev.to e do Medium não está justificando sua existência. É redundante.
Consegue. O POST /public/v1/posts do Postiz suporta provedores de artigo, usando o mesmo endpoint /articles do dev.to que meu cliente nativo chamava. Então o plano: provar isso no Dev.to primeiro (já é um canal conectado no Postiz, baixo risco), depois apagar a integração nativa. Este post é essa primeira fase.
O que eu construí
Um novo Postiz::ArticlePoster. Ele resolve o canal do Dev.to conectado, puxa o markdown do post do GitHub (com o frontmatter removido, a mesma fonte que o código antigo usava), e publica o corpo completo com os metadados do artigo em settings:
{
type: "draft",
posts: [{
integration: { id: devto_channel_id },
value: [{ content: full_markdown_body, image: [] }],
settings: { title:, tags:, main_image:, canonical: }
}]
}
Escrevi direto a partir do código-fonte do provider do Postiz. dev.to.provider.ts lê settings.main_image.path e settings.tags.map(t => t.label), então foi isso que enviei. Testes verdes, payload parecia certo. Publiquei na minha própria máquina e cliquei no botão.
Três coisas que o código-fonte não me contou
1. A API valida de forma mais rígida do que o código que roda. Primeira publicação de verdade, HTTP 400 instantâneo:
posts.0.settings.main_image.id must be a string
posts.0.settings.tags.0.value must be a number
O método post() do provider só lê path e label. Mas a API pública valida a requisição contra um DTO NestJS separado antes do provider sequer rodar, e esse DTO quer mais: main_image tem que ser { id, path }, e cada tag tem que ser { value: <number>, label }. O runtime ignora id e value completamente. Eles existem só para satisfazer a validação. Eu tinha lido o arquivo certo e ainda assim errei, porque o contrato mora em dois arquivos e só um deles faz alguma coisa em tempo de execução. Lição que continuo reaprendendo: com uma API de terceiros, a requisição de verdade é a especificação de verdade.
2. Não existe rascunho no Dev.to. Eu assumi que type: "draft" criaria um rascunho revisável no Dev.to, como o fluxo nativo fazia. Não cria. O provider tem published: true fixo no código, então no momento em que o Postiz processa o post, o artigo já está no ar. type: "draft" só segura ele dentro do Postiz. Então “revisar antes de ir ao público” agora significa revisar na interface do Postiz e publicar de lá, não pré-visualizar um rascunho no Dev.to. Nem melhor nem pior, só diferente, e vale saber antes de prometer a alguém uma etapa de rascunho.
3. A imagem de capa quebrou de um jeito que teste nenhum pega. A segunda publicação passou. Tudo estava perfeito: corpo, blocos de código, as quatro tags, “Originally published at vdaluz.com.” E uma caixa cinza enorme onde devia estar a capa: image no longer exists.
Eu tinha sido um bom engenheiro e feito upload da imagem de destaque para o Postiz primeiro, depois passado a URL do Postiz como capa. Mas meu Postiz roda com STORAGE_PROVIDER=local, os uploads vivem num volume numa máquina na minha casa. Os servidores do Dev.to não conseguem buscar isso de forma durável. O código nativo antigo tinha feito a coisa certa o tempo todo, silenciosamente: ele passava a URL pública original do Pexels direto adiante. Então parei de fazer upload e fiz o mesmo. O id que o DTO exige? Uma string descartável. A capa renderiza agora.
A parte que eu quase errei silenciosamente
Escolher o fluxo de rascunho do Postiz teve uma consequência que eu não vi de início. Meu reconciliador de status já existente faz polling no Postiz numa janela curta orientada a eventos que desiste depois de cerca de uma hora. Isso é ótimo para um post social que publica em minutos. É inútil para um rascunho que fica parado no Postiz até eu ter tempo de publicar ele, o que pode levar dias. Então consolidar tudo no Postiz, que devia remover um job de sincronização, na verdade me obrigou a adicionar de volta uma varredura recorrente. Puxei a lógica correspondente para um Postiz::StatusReconciler compartilhado e apontei dois jobs para ele: o polling rápido orientado a eventos para social, e uma varredura lenta de janela ampla para rascunhos.
O que eu apaguei, e o que vem a seguir
Assim que um artigo de verdade fez o ciclo completo com a capa intacta, apaguei a integração nativa do Dev.to: o cliente, o criador de rascunho, o publisher, o job de sincronização, as colunas de status (migração reversível), os enums, a interface. A mudança líquida da issue inteira foi cerca de 880 linhas a menos, e três mecanismos diferentes de detecção de status colapsaram para um só.
O placar honesto: a construção ficou maior do que o spike sugeria, inteiramente por causa do fluxo de rascunho e das duas surpresas de API. Nada disso apareceu na suíte de testes. Tudo isso apareceu na primeira vez que cliquei no botão contra o sistema de verdade. O Medium vem a seguir, e estou segurando o impulso de apagar por reflexo ali: o token de API dele não pode ser reemitido, então esse caso merece uma decisão deliberada de manter-ou-migrar, em vez do confiante “eu não preciso disso” que pude usar no Dev.to.
Leitura relacionada
Add a feature, or move a responsibility?
Adding Postiz social cross-posting looked done until a blunt question exposed a double-post bug, and a full audit of every posting path in the app found two more like it.
O webhook do Postiz que eu não consegui construir, e o polling em vez dele
Um spike que terminou em "não construa isso": rejeição de IP privado, um payload vazio sem autenticação, e a pull API que tinha tudo que a push API não tinha.
Agendamento e compartilhamento de link mais inteligente no meu cross-poster do Postiz
Agendamento para o futuro, blurbs mais ricos com thumbnails, um seletor de qual URL usar, além do bug de timezone no horário de parede e a importação que copiou a coluna errada.
Você também pode achar útil
AdGuard para iOS
Bloqueio de anúncios e rastreadores em todo o sistema no iOS, sem necessidade de um servidor DNS separado.
Como afiliado da AdGuard, ganho com compras qualificadas.
Saiba maisRackNerd VPS
Hospedagem VPS econômica para serviços leves que funcionam continuamente.
Como afiliado da RackNerd, ganho com compras qualificadas.
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 mais