Desarrollo web
Un fantasma en la home y 19.000 imágenes en cola
Dos frentes el mismo día: un hydration mismatch sin traza en la home y un script para subir 19.000 archivos de WordPress a Payload. Lo que falló y cómo lo resolví.

Un error que solo salía en producción
Después de los últimos cambios, la home empezó a fallar de forma intermitente. En local, nada. En producción, React quejándose con Objects are not valid as a React child. Sin traza útil: hidratación. Cuando el servidor renderiza una cosa y el cliente otra, el error sale así, y el componente culpable casi nunca es el que dice el mensaje.
Lo que cayó, en orden
Primero, km y lectures: después de la fase 2.8 pueden llegar vacíos, y un toLocaleString sobre null arrastraba la página entera. Guard y a otra cosa:
km: d.km != null ? d.km.toLocaleString("ca-ES", { maximumFractionDigits: 1 }) : ""
Después, el culpable de verdad: un texto del AiPanel. El apóstrofo tipográfico de Diga'ns, escapado dentro de una cadena, no se parseaba igual en servidor y cliente. La frase, reescrita:
- Diga'ns les dates i com t'anesa, ...
+ Omple el formulari, i et proposem un pla ...
Y para cerrar, bisect a mano: desactivar <Hero />, <AccountButton />, <BottomNav />, <ConsentBanner />, el script de Umami y el <Header /> entero, un commit por componente. Ninguno era. Todo revertido en bloque. Con el texto corregido y los guards puestos, el fantasma no ha vuelto.
19.000 archivos en cola
Por la tarde, la otra cara: el script T3.3 para migrar los medios de WordPress a Payload. Indexa todos los attachments (imágenes y PDF), los sube a /api/media con 8 workers en paralelo y guarda el mapping wp_id → media_id en un JSON, para que se pueda re-ejecutar sin duplicar nada.
Primera pasada, tres errores.
El mime era mentira. upload_media enviaba todo como image/jpeg hardcodeado. PNG, GIF, WebP y PDF salían con el tipo equivocado. Ahora se infiere por archivo.
El prefijo no era el que parecía. La detección de tablas se quedaba el primer *_posts que encontraba, pero con plugins tipo actionscheduler_ la primera no es la del core. Ahora exige que al lado exista X_postmeta.
La carrera de la sesión. Para subir multipart hay que quitar el Content-Type: application/json de la sesión. Lo hacíamos mutando la sesión compartida, y con 8 workers eso es una race: Payload respondía 400 Invalid JSON al azar. La solución, pasar el header vacío solo en esa llamada:
r = self.session.post(url, files=files, timeout=60, headers={"Content-Type": ""})
Para validar el correctivo sin mover toda la biblioteca, flag nuevo: --only-mime image/png --limit 500.
Qué queda
La home, estable. Los guards, puestos. El script de medios listo para la pasada completa: 19.000 archivos que ya tienen a dónde ir.