Qué pasa cuando un job transmite y nadie escucha
Este issue debía cerrar un círculo que llevaba tiempo abierto en blog-manager: el selector de imágenes de Pexels permite dejar preparada una imagen destacada en un post, pero nada empujaba jamás esa imagen, ni su atribución obligatoria, hacia el post real y publicado. Se elegía una foto, la app la recordaba, y después… nada. Un comentario en el código decía literalmente “Applied to the live article by the direct-repo backfill” junto a una funcionalidad que todavía no existía.
Lo que intenté hacer
Dos cosas, en dos repos distintos. Primero, vdaluz.com necesitaba un lugar donde poner los datos de atribución en su frontmatter y una forma de mostrarlos, “Photo by X on Pexels” debajo de la imagen destacada. Segundo, blog-manager necesitaba escribir de verdad esos datos en el archivo markdown del post publicado en GitHub, usando la capacidad de escritura del issue anterior.
Lo que construí
Del lado de vdaluz.com: un campo opcional heroImageCredit en el esquema de contenido, y un bloque de renderizado que muestra la línea de crédito dentro del mismo contenedor de contenido que la imagen destacada. Esa ubicación no es arbitraria, el importador de historias de Medium solo recoge imágenes que están dentro de la región de texto extraída, así que cualquier cosa fuera de ella desaparece en silencio cuando un post se importa a Medium. La línea de crédito aprovecha esa misma restricción gratis: como se renderiza en el mismo bloque, el importador de Medium la recoge automáticamente cuando extrae la página publicada. No hizo falta ningún código del lado de Medium.
Dev.to es el caso opuesto. Construye el cuerpo del post a partir del archivo markdown crudo, no de la página renderizada, así que una línea de crédito que solo existe como HTML generado por plantilla nunca le llega. Ese caso necesitó una solución explícita, anteponer al cuerpo una línea de crédito con formato markdown antes de enviarlo.
Del lado de blog-manager: un servicio que parchea el frontmatter del archivo publicado, solo por inserción, sin tocar nunca el contenido existente, más un job en segundo plano que lo ejecuta, siguiendo el mismo patrón de reintento/descarte que los jobs de publicación a Dev.to y Medium de la app.
Decisiones que tomé y por qué
Solo inserción, no un reescritor general. Podría haber escrito algo que analizara todo el bloque de frontmatter, fusionara las claves nuevas y lo volviera a serializar. No lo hice, porque un round-trip completo de YAML corre el riesgo de reformatear en silencio cada campo existente, reordenando claves, cambiando el estilo de comillas, lo cual aparece como ruido de diff no deseado en cada post que toca. Como cada post sobre el que corre esto viene de una cola de “falta imagen destacada” (es decir, el archivo demostrablemente todavía no tiene la clave heroImage), la jugada segura fue limitarse a añadir las dos claves nuevas y dejar cada línea existente intacta, byte por byte. Un editor de frontmatter sin pérdida propiamente dicho queda explícitamente como una pieza de trabajo separada y posterior, no quería terminar construyendo por accidente una versión peor de eso como efecto secundario de este issue.
Una validación que revisa el archivo, no la base de datos. Antes de escribir, el servicio vuelve a comprobar si el archivo publicado ya tiene una clave heroImage, usando lo que acaba de obtener de GitHub, no lo que cree la base de datos. Esa distinción resultó importar casi de inmediato (ver más abajo).
Lo que me sorprendió
No tengo un blog de pruebas o descartable en esta app, solo los dos reales. Así que la única forma de verificar manualmente el camino de escritura era apuntarlo al repo real de vdaluz.com, usando su token real de GitHub (por ahora de solo lectura), con la expectativa de que fallara de forma predecible en el paso de escritura y así poder observar que el manejo de fallos funcionaba correctamente.
Falló, pero no de la manera que esperaba. En vez de un error de permisos, chocó con la validación de “este archivo ya tiene una imagen destacada”. La base de datos decía que este post todavía no tenía imagen destacada. El archivo publicado real sí la tenía. En algún punto del camino, los dos se habían desincronizado, probablemente un post al que le agregaron una imagen destacada a mano, fuera de la herramienta, antes de que un rescaneo llegara a notarlo. La validación exacta que había escrito específicamente para desconfiar de un estado obsoleto de la base de datos atrapó una instancia real de ese estado obsoleto en el primerísimo post real contra el que la probé, y se negó a tocar el archivo. Nada se corrompió. Eso se sintió como una buena señal de que la cautela había valido el código extra.
La otra sorpresa surgió de una revisión, no de correr el código. Un pase automatizado que corrí antes de hacer merge marcó que la página alrededor de la cual está construida toda esta funcionalidad, una vista por lotes para recorrer los posts sin imagen destacada, en realidad nunca se suscribe a las actualizaciones del job en segundo plano. El botón se activaba, el job corría, el commit se completaba contra GitHub, y la única página que se usaría de verdad para hacer este trabajo en bloque se quedaba… sin cambios, hasta recargarla manualmente. El mecanismo de transmisión de Turbo no arroja error cuando no hay nadie escuchando, simplemente no llega a ningún lado en silencio. La página de detalle del post sí tenía la suscripción; la página por lotes no. El mismo parcial, renderizado en dos lugares distintos, y solo uno de ellos había quedado conectado para escuchar. Fácil de pasar por alto, porque el clic en sí seguía “funcionando”, solo que no era la versión de funcionar que mostraba algo.
Lo que sigue
Quien retome las imágenes destacadas autoalojadas o los proveedores Unsplash/Openverse podrá construir sobre esto en vez de partir de un estado de “preparado pero nunca confirmado”. Lo único que sigue totalmente fuera de la app: los tokens de GitHub de ambos blogs siguen siendo de solo lectura. Escribir algo de verdad significa entrar en la interfaz de GitHub y volver a delimitar el alcance de un token a mano, no hay API para esa parte.
Lecturas relacionadas
El bug de normalización que solo aparece con etiquetas hechas de nada
Un normalizador basado en strip se topa con una etiqueta de puro signo de puntuación: string vacío como clave de hash, sustitución de etiqueta equivocada, y un autocompletado que hace match con todo. Tres síntomas, una sola causa raíz.
La misma decisión de botón me costó un bug más grande de lo esperado
Incrustar el flujo de imagen destacada en el editor parecía la opción más chica, hasta que 'reemplazar' se topó con 166 archivos reales que nunca habían pasado por el camino de solo inserción, y una migración sin backfill.
El botón de commit del editor es un botón de deploy
Confirmar un borrador a main despliega el blog automáticamente. En cuanto eso quedó claro, sync vs. async dejó de ser una cuestión de estilo, más el caso especial de afiliado heredado que un validador nuevo casi rompió.