El último publicador nativo: terminando la consolidación de Postiz
blog-manager empezó su vida con una integración nativa por plataforma: un cliente de Medium, un cliente de Dev.to, un cliente de Hashnode, un anunciador de LinkedIn, cada uno con su propio flujo de borradores, su job de sincronización y su pila de columnas de estado. En las últimas semanas las fui eliminando una por una y enrutando todo a través de Postiz, el programador autoalojado que ya maneja mis posts en redes sociales. Esta semana cayó el último. Medium desapareció, al menos el código nativo, y blog-manager ahora habla con exactamente un solo sistema externo.
Por qué Medium fue el último
Dev.to fue el modo fácil: tiene una API que funciona, Postiz tiene un proveedor de Dev.to que funciona, y migrar fue sobre todo trabajo de payload. Medium es lo opuesto. Medium dejó de emitir tokens de API nuevos y yo nunca tuve uno de antes del corte, lo que significa que mi “integración nativa de Medium” eran cuatrocientas líneas de código que no podían publicar nada. No era una integración. Era una pieza de museo.
Ya no existe un camino de API hacia Medium. La entrada, construida en un proyecto de homelab separado, es un pequeño sidecar junto a Postiz: un navegador headless parchado en modo sigiloso que mantiene una sesión de Medium con la sesión iniciada, manejando la función oficial Import a story de Medium. El proveedor de Medium de Postiz está parchado para llamar a ese sidecar en vez de a api.medium.com. Desde el punto de vista de blog-manager, nada de esa maquinaria es visible, simplemente programa un post al canal de Medium como cualquier otro.
Un artículo cuyo cuerpo es una URL
El detalle de diseño interesante: para Dev.to, blog-manager envía el cuerpo completo en markdown más título, etiquetas, portada y URL canónica. Para Medium envía casi nada, la URL canónica pública del post como si fuera el contenido. El puente pega esa URL en el cuadro de importación de Medium, y Medium mismo obtiene la página, extrae el artículo, y fija rel=canonical de vuelta a mi sitio. Cuanto menos envío, menos puede desincronizarse. El propio importador de Medium es el motor de renderizado, así que la fidelidad del formato es problema de Medium, no mío.
Eso hizo que el cambio de código fuera pequeño. El servicio ArticlePoster existente, de la migración de Dev.to, pasó a conocer el proveedor: una rama construye el payload de Dev.to (cuerpo + settings), la otra envía la URL. El seguimiento de estado no necesitó ningún cambio, las entradas de ambos proveedores caen en el mismo mapa de publicación por canal, y el mismo job de reconciliación recurrente vigila que pasen de borrador a publicado.
Mordido dos veces, pero más rápido la segunda
Envié el primer post de Medium con settings vacíos, razonando que como Medium extrae todo de la URL, no había nada que configurar. Postiz lo rechazó: settings.title should not be null… settings.subtitle should not be null.
Este es exactamente el mismo modo de falla que me mordió durante la migración de Dev.to: Postiz valida la petición contra un DTO de settings antes de que el proveedor siquiera corra, y el DTO exige campos que el runtime apenas usa. La vez pasada perdí una ronda entera de pruebas en vivo por esto. Esta vez reconocí el error a primera vista, saqué el MediumSettingsDto del código fuente de Postiz, vi que title y subtitle son obligatorios con un mínimo de dos caracteres, y envié la solución en minutos. Hay algo satisfactorio en un bug que cuesta una hora la primera vez y cinco minutos la segunda, significa que esa primera hora sí compró algo. (También ayudó haber escrito el gotcha en mi base de conocimiento después de la primera ronda, incluyendo la instrucción “leer tanto el provider COMO el settings DTO”. El yo del pasado dejó una nota justo donde el yo del futuro iba a tropezar.)
Un detalle sutil: el título no es solo para apaciguar al DTO. El importador de Medium descarta el título en silencio, y el puente lo vuelve a aplicar después de la importación a partir de ese campo de settings. Así que el campo obligatorio que parecía puro trámite en realidad es estructural.
La hoguera
Después de que un post real completara el ciclo completo (blog-manager lo dejó preparado, Postiz se lo entregó al puente, el puente lo importó y lo publicó, el artículo salió con el título correcto, el cuerpo, el enlace canónico y la lista de referencias) eliminé la integración nativa. Seis clases de servicio, dos jobs, un endpoint de webhook, un poller de RSS, siete columnas en posts y dos en blogs, la interfaz de credenciales, una tarjeta de dashboard, y una sincronización recurrente. Neto: menos 1.603 líneas.
Contando todo el arco (Hashnode, LinkedIn, Dev.to, Medium), blog-manager se deshizo de cada cliente de plataforma que alguna vez tuvo. Lo que queda es un cliente de Postiz, un article poster con dos ramas pequeñas por proveedor, y un loop de reconciliación. Tres mecanismos distintos de sincronización de estado se convirtieron en uno.
Lo que le diría a mi yo del pasado
Eliminar código que no puede correr. La integración nativa de Medium estuvo ahí meses pareciendo una funcionalidad que funcionaba. Su token nunca se podía volver a emitir. El código que no puede tener éxito es peor que no tener código, hace que la interfaz mienta.
Cuando aparece una falla rara de un tercero, hay que anotarla donde se vuelva a tropezar la próxima vez. La nota sobre el DTO más estricto que el runtime se pagó sola en un solo issue.
La consolidación se acumula. Cada migración hizo que la siguiente fuera más pequeña. Dev.to requirió construir el camino del artículo, el barrido de reconciliación y la fila de la interfaz. Medium reutilizó todo eso y fue sobre todo una rama de payload y un diff de eliminación.
La única dependencia nueva es real: toda la publicación ahora fluye a través de mi instancia de Postiz en el homelab, y Medium en particular depende de una sesión de navegador que eventualmente va a expirar. Ese es un trade que hice a sabiendas, un solo motor que mantener en vez de cuatro integraciones que cuidar, con un botón de importación manual como respaldo si el puente se rompe a mitad de semana.
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.