Saltar al contenido
Development

Borrando una integración al moverla a Postiz

Por Victor Da Luz
railsrubypostizdevtodev-logblog-manager

blog-manager creció con personalidad dividida. Las republicaciones de artículos completos en Dev.to y Medium tenían cada una su propio cliente de API, su flujo de borrador-y-luego-publicar, y su job de sincronización. Todo lo social, Bluesky, Mastodon, X, LinkedIn, pasaba por Postiz, un programador autoalojado. La línea entre “nativo” y “Postiz” no fue una decisión de diseño. Fue simplemente el orden en el que construí las cosas.

Cuando arranqué Hashnode hace poco, la duplicación se volvió difícil de ignorar. Dos de mis integraciones eran ~80% el mismo código repetitivo: un cliente, un creador de borradores, un job de sincronización, columnas de estado por plataforma. Así que corrí un spike con una pregunta directa: ¿Postiz realmente publica artículos completos con URLs canónicas, o solo blurbs sociales cortos? Si puede publicar artículos, mi código nativo de Dev.to y Medium no se está ganando su lugar. Es redundante.

Puede. POST /public/v1/posts de Postiz soporta proveedores de artículos, usando el mismo endpoint /articles de dev.to que llamaba mi cliente nativo. Así que el plan: probarlo primero en Dev.to (ya es un canal de Postiz conectado, bajo riesgo), después borrar la integración nativa. Este post es esa primera fase.

Lo que hice

Un nuevo Postiz::ArticlePoster. Resuelve el canal de Dev.to conectado, obtiene el markdown del post desde GitHub (sin el frontmatter, la misma fuente que usaba el código viejo), y publica el cuerpo completo con los metadatos del artículo en settings:

{
  type: "draft",
  posts: [{
    integration: { id: devto_channel_id },
    value:    [{ content: full_markdown_body, image: [] }],
    settings: { title:, tags:, main_image:, canonical: }
  }]
}

Lo escribí directamente a partir del código fuente del proveedor de Postiz. dev.to.provider.ts lee settings.main_image.path y settings.tags.map(t => t.label), así que eso fue lo que envié. Pruebas en verde, el payload se veía bien. Lo desplegué en mi propia máquina y apreté el botón.

Tres cosas que el código fuente no me dijo

1. La API valida más estricto que el código que corre. Primera publicación real, HTTP 400 instantáneo:

posts.0.settings.main_image.id must be a string
posts.0.settings.tags.0.value must be a number

El método post() del proveedor solo lee path y label. Pero la API pública valida la solicitud contra un DTO de NestJS separado antes de que el proveedor llegue a correr, y ese DTO pide más: main_image tiene que ser { id, path }, y cada tag tiene que ser { value: <number>, label }. El runtime ignora id y value por completo. Existen solo para satisfacer la validación. Había leído el archivo correcto y aun así lo hice mal, porque el contrato vive en dos archivos y solo uno de ellos hace algo en tiempo de ejecución. Lección que sigo reaprendiendo: con una API de terceros, la solicitud real en vivo es la especificación de verdad.

2. No existe un borrador en Dev.to. Asumí que type: "draft" crearía un borrador revisable en Dev.to, como hacía el flujo nativo. No es así. El proveedor tiene fijo published: true, así que en el momento en que Postiz procesa el post, el artículo queda publicado. type: "draft" solo lo retiene dentro de Postiz. Así que “revisar antes de que salga al público” ahora significa revisar en la interfaz de Postiz y publicar desde ahí, no previsualizar un borrador en Dev.to. Ni mejor ni peor, simplemente distinto, y vale la pena saberlo antes de prometerle a alguien un paso de borrador.

3. La imagen de portada se rompió de una forma que las pruebas no pueden atrapar. La segunda publicación se completó. Todo estaba perfecto: el cuerpo, los bloques de código, las cuatro etiquetas, “Originally published at vdaluz.com.” Y una gran caja gris donde debía ir la portada: image no longer exists.

Había sido un buen ingeniero y subí primero la imagen hero a Postiz, después pasé la URL de Postiz como portada. Pero mi Postiz corre con STORAGE_PROVIDER=local, las subidas viven en un volumen en una máquina en mi casa. Los servidores de Dev.to no pueden traer eso de forma durable. El código nativo viejo había estado haciendo lo correcto en silencio todo este tiempo: pasaba directo la URL pública original de Pexels. Así que dejé de subir la imagen y hice lo mismo. ¿El id que exige el DTO? Un string descartable. La portada se renderiza ahora.

La parte que casi hice mal en silencio

Elegir el flujo de borrador de Postiz tuvo una consecuencia que no vi al principio. Mi reconciliador de estado existente sondea Postiz en un horario corto, dirigido por eventos, que se rinde después de más o menos una hora. Eso está bien para un post social que se publica en minutos. Es inútil para un borrador que se queda en Postiz hasta que yo me acuerde de publicarlo, lo cual puede tomar días. Así que consolidar todo en Postiz, que se suponía que iba a eliminar un job de sincronización, en realidad me obligó a agregar de nuevo un barrido recurrente. Pasé la lógica de coincidencia a un Postiz::StatusReconciler compartido y apunté dos jobs hacia él: el sondeo rápido de eventos para lo social, y un barrido lento de ventana amplia para los borradores.

Lo que borré, y lo que sigue

Una vez que un artículo real completó el ciclo con la portada intacta, borré la integración nativa de Dev.to: el cliente, el creador de borradores, el publicador, el job de sincronización, las columnas de estado (migración reversible), los enums, la interfaz. El cambio neto para todo el issue fue de unas 880 líneas menos, y tres mecanismos distintos de detección de estado colapsaron en uno solo.

El balance honesto: la construcción fue más grande de lo que el spike sugería, enteramente por el flujo de borrador y las dos sorpresas de la API. Nada de eso apareció en la suite de pruebas. Todo apareció la primera vez que apreté el botón contra el sistema real. Medium sigue, y me estoy conteniendo de una eliminación por reflejo ahí, su token de API no se puede reemitir, así que esa se gana una decisión deliberada de mantener-o-migrar en vez del confiado “no necesito esto” que pude usar en Dev.to.

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