Saltar al contenido
Development

Eliminar una integración de publicación que acababa de construir

Por Victor Da Luz
railsrubyhashnodepostizdev-logblog-manager

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

Development

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.

Leer