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
Una corrección de desfase de documentación que no fue tan aburrida como sonaba
Tres puntos de auditoría que se volvieron algo más: una afirmación a medio corregir, un restablecimiento de contraseña muerto en silencio, y un correo de staging que enlazaba a producción.
Un 500 escondido dentro de las rutas aisladas de un engine montado
El dashboard de jobs devolvía un 500 en vez de una página de login: los route helpers sin calificar se resuelven contra el engine, no contra la app. Una línea, más su gemela dormida.
Borrando código muerto, y descubriendo una razón equivocada para una respuesta correcta
Un issue de limpieza con una justificación equivocada, un barrido de documentación que no hacía falta, y la credencial huérfana que atrapó una revisión.
También te podría ser útil
RackNerd VPS
Alojamiento VPS económico para servicios ligeros que funcionan de forma continua.
Como afiliado de RackNerd, obtengo ingresos por las compras que califican.
Más informaciónProton VPN
VPN comercial con filtrado NetShield e interruptor de apagado automático.
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ón