Notes
A ghost on the home page and 19,000 images in the queue
Two fronts on the same day: a hydration mismatch with no trace on the home page and
3 min read
An error that only showed up in production
After the latest changes, the home page started crashing intermittently. Locally, nothing. In production, React complaining with an Objects are not valid as a React child. No useful trace: hydration (the server renders the page and the browser "re-checks" it to make it live; if two different things come out, it crashes). When the server renders one thing and the client another, the error pops up like that, and the offending component is almost never the one the message points to.
What fell, in order
First, km and lectures: after phase 2.8 they can come in empty, and a toLocaleString on null was dragging down the entire page. Guard (a check that prevents working with empty data) and done:
km: d.km != null ? d.km.toLocaleString("ca-ES", { maximumFractionDigits: 1 }) : ""
Then, the real culprit: a text from the AiPanel (LaIA's assistance: LaIA inside: plans, assistance and passwords). The typographic apostrophe in Diga'ns, escaped inside a string, didn't parse the same way on server and client. The sentence, rewritten:
- Diga'ns les dates i com t'anesa, ...
+ Omple el formulari, i et proposem un pla ...
And to wrap up, manual bisect (disabling components one by one to find the culprit): disabling <Hero />, <AccountButton />, <BottomNav />, <ConsentBanner />, the Umami script and the entire <Header />, one commit per component. None of them was it. All reverted in bulk. With the text fixed and the guards in place, the ghost hasn't come back.
19,000 files in the queue
In the afternoon, the other side: the T3.3 script to migrate WordPress media over to Payload, the migrator that already talks to the production WordPress (The migrator learns to talk to Payload). It indexes all attachments (images and PDFs), uploads them to /api/media with 8 workers (parallel processes) and saves the wp_id → media_id mapping in a JSON (a text file with data), so it can be re-run without duplicating anything.
First run, three errors.
The mime was a lie. upload_media was sending everything with image/jpeg hardcoded (hammered into the code). PNG, GIF, WebP and PDF went out with the wrong type. Now it's inferred per file.
The prefix wasn't what it seemed. The table detection was grabbing the first *_posts it found, but with plugins like actionscheduler_ the first one isn't the core one. Now it requires X_postmeta to be next to it.
The session race. To upload multipart you need to drop the Content-Type: application/json from the session. We were doing it by mutating the shared session, and with 8 workers that's a race (two processes stepping on each other): Payload was replying with 400 Invalid JSON at random. The fix: pass the header empty only in that call:
r = self.session.post(url, files=files, timeout=60, headers={"Content-Type": ""})
To validate the fix without moving the whole library, new flag: --only-mime image/png --limit 500.
What's left
The home page, stable. The guards, in place. The media script ready for the full pass: 19,000 files that already have somewhere to go.