Commits

The migrator now talks to the production WordPress

First pilot against the real WordPress: dual authentication, table prefixes,

The migrator (the script that moves content from the old WordPress to the new manager) has been working with test data for three weeks. Today I put it in front of the real WordPress: the production database, with its weird table prefixes and Toolset taxonomies (WordPress tools for classifying content). Phase 3 is no longer a draft: the pilot imports.

Two doors to enter Payload

The first problem wasn't even about migration: it was about authentication (proving to the system who you are). Payload's API keys plugin —keys that identify one program to another— requires manual configuration from the interface, and I wanted a path that always works. Solution: dual mode. If PAYLOAD_API_KEY is present, use the API key; otherwise, log in with email and password (JWT, an access token), which works out-of-the-box. And if there's nothing at all, the script stops with a RuntimeError that explains it.

Finding the right tables

To detect the WordPress table prefix I was running a LIKE '%\_posts'. PyMySQL (the library that runs SQL queries from Python) eats it as a format specifier, and on top of that the first candidate that came up was a Mailpoet table, not the core. Two fixes: escape the percent sign (%%) and cross-reference with *_options tables, which are the ones that betray the WordPress core. If there are several candidates, the shortest prefix wins.

Counties, municipalities, and unforgiving details

In the first pilot nothing was linking: the counties weren't resolving to IDs, the seed said berguedà with an accent and WordPress has it without, and Payload wanted integers where I was passing strings. Now the county and municipality catalogs are created on their own (idempotent: re-running them doesn't duplicate anything) and relationships go in with the correct type. For Bagà, 13 posts came out: 1 culture post and 12 calendar events.

From Gutenberg HTML to Lexical

The WP content arrived with <!-- wp:paragraph --> comments that Payload was escaping and showing literally on the page. I rewrote the converter with html.parser: it cleans up Gutenberg comments, maps paragraphs, headings, and lists, and preserves bold and italics within the text. Links also required extra fields on the node (id, direction, format): without them, the admin crashed with a React error #418 and hid all richText fields. With the new parser, the two pieces from the latest pilot render cleanly on the frontend.

On the side: dependencies up to date

I took the chance to bump Payload to 3.89, Next to 15.4.11, and React to 19.0.1: Payload's peer dep (the versions it requires from its neighbors) demanded it. Error #418 survived the upgrade, which was another hint that it was structural and not version-related.

Day's tally: 29 smoke tests green (quick checks of the basics), two pilots imported with cover and meta description, and the feeling that phase 3 finally walks on its own. The continuation, at The densest Saturday: editor, ingest, and a DNS that went down; advertising would also cross the bridge at Advertising gets imported: Advanced Ads into Payload.