¿Agregar una función o mover una responsabilidad?
Esta semana agregué republicación cruzada a redes sociales en mi blog manager. Ya funciona, pero la versión que publiqué primero tenía un bug que no detecté hasta que alguien me hizo una pregunta directa: “no recuerdo haber pedido esto. ¿Por qué lo implementaste?” La respuesta honesta es que había confundido dos tareas distintas, y la diferencia vale la pena escribirla.
El punto de partida
Mi blog manager ya republica artículos en Medium, Dev.to y Hashnode, y anunciaba publicaciones nuevas en LinkedIn a través de la propia API de LinkedIn. Corro una instancia autoalojada de Postiz para programar publicaciones sociales, y el plan era enrutar los canales sociales, Bluesky, Mastodon, X, LinkedIn, a través de ella. Mantener las plataformas de blog nativas, mover lo social a Postiz. Una separación limpia.
La construcción en sí fue simple. Un cliente pequeño sobre la API pública de Postiz:
def create_post(payload:)
request(Net::HTTP::Post, "/posts", payload)
end
La autenticación resultó ser la clave de API cruda en el encabezado Authorization, sin prefijo Bearer. El cuerpo del post quiere un settings: {} por canal, y el servidor completa el tipo de proveedor por su cuenta, así que un solo payload genérico funciona para cada canal. Armé un servicio para construir el payload, un job para correrlo, un campo de configuración para la clave, y un selector “Compartir en redes” en cada post. Pruebas en verde, rubocop limpio. Lo documenté y lo entregué.
El bug que publiqué
Acá está la parte que hice mal. La ruta vieja de anuncio a LinkedIn seguía ahí, corriendo en un cron diario. Mi nueva ruta de Postiz también podía publicar en LinkedIn. Así que un solo post podía salir hacia LinkedIn dos veces, una por la vía vieja, otra por la nueva. Incluso había notado la superposición mientras lo construía, y en vez de resolverla dejé una nota llamándola “decisión futura” y seguí adelante.
Ese fue el momento en que aterrizó la pregunta. El issue era mover la publicación social a Postiz. Yo había agregado Postiz junto a lo que ya existía. Suenan parecido pero no son el mismo verbo. Agregar es puramente constructivo: escribir lo nuevo, publicarlo. Mover tiene una segunda mitad fácil de saltarse: hay que ir a retirar lo viejo. Yo había hecho la mitad divertida y le hice un gesto a la otra mitad.
Auditando el resto
En vez de simplemente borrar la única ruta de LinkedIn que conocía, salí a buscar todo lo que publica en un servicio externo en cualquier parte de la app: cada job, cada cron, cada acción de controlador, cada callback de modelo. Quería el mapa completo, porque si se me había pasado una superposición probablemente se me habían pasado otras.
Así fue. Surgieron tres cosas:
- El anuncio nativo a LinkedIn se disparaba automáticamente desde tres jobs de sincronización diaria distintos, no uno solo. Retirarlo significaba tocar los tres, además de las columnas del modelo, la interfaz, y un montón de pruebas.
- Mi selector de canal nuevo listaba todos los canales conectados a Postiz. Había conectado Dev.to a Postiz para hacer pruebas, así que el selector me habría dejado tranquilamente publicar en Dev.to a través de Postiz mientras ya se publicaba de forma nativa. Otro doble post, uno que yo mismo había construido.
- Un post programado a través de Postiz mostraba “programado” para siempre, porque la pieza que confirma que en verdad se publicó vive en un issue aparte que todavía no construí.
El del selector dolió un poco. Había agregado exactamente el riesgo de duplicación que se suponía que estaba eliminando, solo que en otra plataforma.
El arreglo, y la lección
Así que hice el movimiento como corresponde. La ruta nativa de LinkedIn desapareció: el servicio, las columnas, la interfaz, las pruebas, todo, y una migración elimina las columnas. LinkedIn ahora solo es alcanzable a través de Postiz. El selector filtra las plataformas de blog para que no pueda duplicar el flujo nativo. Y la responsabilidad de “publicar en redes sociales” vive en exactamente un solo lugar.
Lo que sigo reaprendiendo: cuando una tarea dice “mover X a Y”, el trabajo está sobre todo en encontrar y eliminar la X vieja, no en construir Y. Construir Y es visible, satisfactorio, y da algo para mostrar. Eliminar X es invisible, es la ausencia de un doble post que nadie te va a agradecer nunca. Pero es el verdadero punto de la tarea. Me lo salté porque no se sentía como progreso, y “no se siente como progreso” es una razón terrible para dejar una trampa en el código.
También me hizo apreciar el valor de mapear todo el sistema antes de dar por terminado un “mover”. No podría haber listado esas tres superposiciones de memoria. Tuve que ir a leer cada ruta de publicación en la app, y solo entonces pude ver que “el cron de LinkedIn” en realidad era tres crons y un bug de selector. Un movimiento no está terminado cuando la ruta nueva funciona. Está terminado cuando las rutas viejas desaparecieron, y solo sabés que desaparecieron si fuiste a mirar.
Qué sigue
La versión actual publica de inmediato con un título y un enlace. Quiero programar publicaciones para después, traer la descripción, y adjuntar una miniatura, ese es el próximo issue. Y la confirmación de “¿en verdad se publicó?” todavía necesita su webhook. Pero la base ya es honesta: una responsabilidad, un solo lugar, sin duplicados silenciosos.
Lecturas relacionadas
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, blurbs más completos con miniaturas y un selector de URL, más el bug de zona horaria y la importación que copió la columna equivocada.
El último publicador nativo: terminando la consolidación de Postiz
La integración nativa de Medium era una pieza de museo. Un puente de navegador, un artículo cuyo cuerpo es una URL, y menos 1.603 líneas.
También te podría ser útil
Proton Drive
Almacenamiento en la nube cifrado, del equipo detrás de Proton Mail.
Como socio de Proton, obtengo ingresos por las compras que califican de los servicios de privacidad y seguridad de Proton (Pass, Mail, VPN, Drive).
Más informaciónProton Mail
Correo electrónico cifrado de extremo a extremo, con arquitectura de acceso cero.
Como socio de Proton, obtengo ingresos por las compras que califican de los servicios de privacidad y seguridad de Proton (Pass, Mail, VPN, Drive).
Más informaciónProton Pass
Gestor de contraseñas centrado en la privacidad, del equipo detrás de Proton Mail.
Como socio de Proton, obtengo ingresos por las compras que califican de los servicios de privacidad y seguridad de Proton (Pass, Mail, VPN, Drive).
Más información