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 very strange prefixes and the 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 worked. Solution: dual mode. If there is a PAYLOAD_API_KEY, use API key; if not, log in with email and password (JWT, an access token), which works out-of-the-box. And if there is nothing at all, the script halts with a RuntimeError that explains it.

Finding the right tables

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

Counties, municipalities, and details that don't forgive

In the first pilot nothing linked up: 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 create themselves (idempotent: running them again doesn't duplicate anything) and the relationships carry the correct type. For Bagà, 13 posts came out: 1 cultural piece and 12 calendar events.

From Gutenberg HTML to Lexical

The WP content was arriving with <!-- wp:paragraph --> comments that Payload was escaping and displaying literally on the page. I've rewritten the converter with html.parser: it cleans up the Gutenberg comments, maps paragraphs, headings, and lists, and preserves bold and italics within the text. The links, furthermore, 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 last pilot look clean on the frontend.

While at it: dependencies up to date

I took the chance to update Payload to 3.89, Next to 15.4.11, and React to 19.0.1: Payload's peer dep (the versions it demands of its neighbors) required it. Error #418 survived the update, which was another clue that it was structural, not a version issue.

Day balance: 29 smoke tests green (quick checks of the basics), two pilots imported with cover image and meta description, and the feeling that phase 3 is finally walking on its own. Continuation, in The densest Saturday: editor, ingestion, and a DNS that went dark; the advertising would also end up crossing the bridge in The advertising imports: Advanced Ads into Payload.