Eliminar una integración de publicación que acababa de construir
Hace unas semanas conecté Hashnode a blog-manager: un cliente GraphQL, un creador de borradores, un job de sincronización de estado de publicación, un publisher, columnas en dos tablas, campos de formulario, insignias de estado, un cron nocturno. Esta semana eliminé todo eso. Acá está el porqué, y lo que eliminar una funcionalidad me enseñó sobre la forma que realmente quiero.
Lo que intentaba hacer
blog-manager sindica mis posts desde GitHub hacia otras plataformas. El plan original era “una integración nativa por plataforma”: Medium, Dev.to, Hashnode, LinkedIn, cada una con su propio cliente de API y su propio flujo de publicación. Es un modelo mental limpio hasta que se tienen cuatro y se nota que el 80% es el mismo código con nombres de campo distintos.
Después agregué Postiz (un programador social autoalojado) para el lado social, y la duplicación empeoró. Un post de blog ahora podía llegar a LinkedIn de dos formas: de forma nativa, o como canal de Postiz. Ya había sacado el camino nativo de LinkedIn justo por ese motivo. Hashnode era la siguiente ficha de dominó. Nunca conecté una cuenta real de Hashnode, así que era puro peso muerto: código que tendría que mantener funcionando en cada refactor futuro para una plataforma que no uso.
Lo que eliminé
El grep fue la contabilidad honesta. 31 archivos tocaban hashnode:
app/services/hashnode/{client,draft_creator,published_sync,publisher}.rb
app/jobs/hashnode_{import,sync}_job.rb
lib/tasks/hashnode.rake
+ columns on posts (7) and blogs (3), two indexes
+ enums, controller actions, routes, a nightly cron
+ form sections, table columns, filters, a status-badge preset
+ all the tests for the above
Diff final: 31 archivos, +14 / -1126. Eliminar funcionalidades es la única vez que el conteo de líneas se mueve en la dirección satisfactoria.
La migración replicó la de LinkedIn: un remove_column/remove_index reversible con los argumentos de tipo completos, para que revierta limpio:
def change
remove_index :posts, :hashnode_status
remove_index :posts, :hashnode_import_state
remove_column :posts, :hashnode_status, :integer, default: 0, null: false
# ...7 post columns, 3 blog columns
end
Decisiones que tomé en el camino
El grep mintió un poco, y ahí está lo interesante. Mi primera pasada filtró por extensión de archivo: .rb, .erb, .yml, .md. Se saltó lib/tasks/hashnode.rake porque .rake no estaba en mi lista. Solo lo detecté en una segunda pasada, sin filtro, al final. Lección que sigo reaprendiendo: cuando se elimina algo “por completo,” el grep de verificación tiene que ser más amplio que el grep de edición. Lo que se olvida buscar es lo que sobrevive.
Una decisión que el ticket dejó explícitamente abierta: una constante llamada NATIVE_BLOG_PROVIDERS excluye a Medium/Dev.to/Hashnode del selector de canales de Postiz, para que un post no pueda cruzarse dos veces (artículo completo de forma nativa más un texto corto vía Postiz). Una vez que Hashnode nativo desaparece, ¿debería hashnode seguir en esa lista de exclusión? Lo quité. Ya no hay riesgo de doble publicación, y quitarlo significa que si algún día se conecta un canal de Hashnode en Postiz, queda seleccionable como cualquier otro destino social. Esa elección adelanta, sin decirlo del todo, hacia dónde va todo esto.
La especificación del ticket era una sugerencia, no un mapa. Enumeraba columnas y archivos para eliminar, pero era anterior a una funcionalidad posterior (el selector de origen de URL) que ya había agregado hashnode_url a SHARE_SOURCES, shareable_urls, y share_url del modelo Post. Ninguno de esos estaba en la lista de verificación de eliminación. “Eliminarlo por completo” solo funciona si se vuelve a derivar el radio de impacto a partir del código actual, no de un ticket escrito contra un árbol más viejo.
Lo que me sorprendió
Eliminar Hashnode no se sintió como limpieza. Se sintió como si el código estuviera haciendo una pregunta que había estado evitando: ¿por qué tengo publishers nativos, para empezar?
Por ahora, Medium y Dev.to siguen publicando de forma nativa: artículo completo, mi propio cliente de API, mi propia danza de borrador y luego publicación. Todo lo social pasa por Postiz. Esa división tenía sentido cuando la construí (“plataformas de blog nativas, social vía Postiz”), pero después de eliminar Hashnode no puedo defender la frontera con claridad. No es “blog contra social,” es “las dos que construí primero contra todo lo demás.” Eso es historia, no arquitectura.
Lo que sigue
Voy a abrir un spike para definir el flujo correcto de publicación y difusión de punta a punta: qué plataformas se quedan nativas y por qué, cuáles se mueven detrás de Postiz, y qué debería significar siquiera la acción de publicar cuando un post se ramifica en una URL canónica más N cross-posts más M textos sociales. Eliminar una integración fue fácil. Decidir qué deberían ser las que quedan es el trabajo real. Más sobre esto cuando el spike aterrice.
Lecturas relacionadas
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.
El webhook de Postiz que no pude construir, y el sondeo en su lugar
Un spike que terminó en "no construir esto": rechazo por IP privada, un payload vacío sin autenticación, y la API de lectura que tenía todo lo que le faltaba al push.
Programación y enlaces más inteligentes para mi publicador cruzado de Postiz
Programación a futuro, descripciones más completas con miniaturas, y un selector de qué URL usar, más el error de zona horaria del reloj de pared y la importación que copió la columna equivocada.