
In late July I decided to rebuild this blog from scratch, and to do it with an AI. Not occasional help with a snippet: the whole redesign, the English translation, the SEO, the migration to production and the server maintenance.
It came to 72 commits in four days, 250 files and more than 18,000 lines. The result is measurable and it is better than what was there. And along the way four things broke, one of them in production, live.
This article tells both halves. The first gets told a lot; the second almost never, and it is the one that is actually useful.
What went well
Starting here, because without it the rest sounds like a complaint, and it is not. The site came out objectively better, and that is not an impression: these are figures anyone can check by visiting.

The last line deserves a caveat, because it is more interesting than it looks. I did have backups: the whole container is backed up with Proxmox Backup Server, which is what plenty of people do and is fine. What I did not have was a logical dump of the database, the kind you can pull a single table out of. It sounds like a minor distinction. Further down it turns out not to be.
The first thing to break: a pattern that was too greedy
Renaming 125 image files to descriptive names looked like the dullest task of the lot. The catch is that WordPress does not store an image, it stores a set: the original plus the sizes it generates from it.
To rename the whole set, the script asked for “every file starting with the old name”. With a screenshot of mine called login.png, that correctly finds login-1024x576.png and login-300x169.png. The problem is that the same folder held three other screenshots with no relation to the first: login1.png, login-npm.png and loginadmin.png. All of them start with “login”. It swept them up.
There was a second, nastier effect: when the new name began with the old one, the pattern picked up the file it had just created and renamed it again. That is how a ollama-listar-modelosar-modelos.png came to exist. The mistake was not picking the wrong name, it was using “starts with” without requiring a separator to follow.
Total damage: 24 original images with mangled names and 68 derivatives. And the script finished by reporting 97 files renamed successfully. No exception, no warning, no error. It surfaced only when checking afterwards, one by one, that all 141 image URLs still responded.
The second: deleting rows from a schema that was not what I thought
Database cleanup. There were orphan rows in the term relationships table, the one linking a post to its categories. The code compared each row against the posts table and deleted those whose post did not exist. 83 rows.
The trouble is that in that table the column I assumed was a post identifier is not always one. Polylang, the plugin handling the two languages, uses it to store each term’s language and translations.
Result: six of my ten categories lost their language. The five English ones started returning 404. In production, with the site already published.
It surfaced while verifying another change: checking the title of a category archive, it read “Page not found” where the name should have been. And had it not surfaced there, it would have come out twenty-two minutes later anyway: the full review of all 54 URLs on the site, which includes the ten category ones, was already scheduled and ran at 17:59. It was fixed by restoring that one table from the 17:24 backup.
The third: three minutes blank, with the status code saying everything was fine
The cutover to production happened at 16:50. I copied the child theme, the database, the media, everything. I forgot the parent theme.
A WordPress child theme cannot render anything without its parent. What is interesting is how it fails: it is not recorded as a fatal error.

Status 200. Zero bytes of body. Correct headers, language cookie set, and not a single line in the PHP log or the nginx log. The page was blank and everything you could query said it was fine.
It was caught by the post-change verification, which measures the content of each page and not just its status code. That is why it showed up: had the check simply asked “does it return 200?”, the way almost every monitor does, it would have signed the site off as healthy. Seventeen minutes between the cutover and the fix.
The fourth: acting before reading the check
This one is different, which is why it comes last. I was about to drop some orphan tables from plugins that had already been uninstalled. First I had to check whether any installed plugin used them. I wrote the check and the deletion in the same command.
The check said yes, Rank Math uses one of them. But the deletion had already run on the same line, so I read the warning after the damage. It was restored from a dump made one step earlier.
That was not a knowledge failure. It was a process one. Verifying and acting have to be separate, or the verification is decorative.
None of the four was found by chance
This is the part I most want to be clear about, because it is the opposite of how these stories usually go. None of the four came to light through a lucky glance or because somebody happened to walk past. All four were found by a check that existed for exactly that purpose, and none of them reached a reader.
The discipline was not improvised along the way: it was part of the brief. These five things, all fairly boring, were in place from the start.
- A backup before every destructive step. Not one general copy and done: a specific dump right before touching anything, even for operations that looked trivial.
- Automatic backups that are tested by restoring. The 17:24 one saved the categories. Had it merely been written to disk without ever checking it could be restored, it would have been worth nothing.
- Dry runs. Almost every destructive script ran first in simulation mode, showing what it would do without doing it. Several errors were caught there and never made this list.
- Verifying by measuring, not by looking. After each change, checking the URLs one by one instead of trusting the script’s “Success”. All four failures were caught here: in three of them the script had finished reporting that everything had gone fine.
- Least privilege. None of this touched production until the end, and the staging environment never had access to the real site’s email or analytics.
And now what I actually learned
The four failures above were found by the machine itself, checking its own work. But there were two more that it did not find, and those are the interesting ones.

The first was the header logo. It rendered at natural size: a 473-pixel-tall header, impossible to miss on the homepage. There was a harness walking 54 URLs and measuring structure, links, images and structured data, and it reported zero problems. It had been like that for days in staging.
The second was dark mode contrast. Section headings were nearly invisible. Measuring turned up between 19 and 23 failures per page, one of them at a contrast ratio of 1.08 to 1 where the minimum is 3. It was fixed, measured again, reported zero. And it was still broken: if your system is in dark mode and you press the site’s own toggle to view it light, it failed again. The check tested two states when there are in fact four.
My conclusion is not that automated verification is useless. It found all four failures in the first half of this article, and without it two of them would have reached production unnoticed.
The conclusion is more uncomfortable: the automated verification did not fail because it was bad. It failed because it measured whatever occurred to it to measure. It checked that images loaded, not that they were a reasonable size. It checked contrast in two themes, not in all four possible combinations. Every check is a hypothesis about what might go wrong, and what does not occur to you does not get measured.
A human eye on the screen found in two seconds what that harness was signing off. Not because it is smarter, but because it does not start from a list.
Would I do it again?
Yes, and with the same precautions, which turned out to be exactly the ones that were needed.
What I would change is the idea that automated verification replaces looking. It does not. They are two different things: one scales, the other discovers. A machine can check 54 pages in two minutes and never tires; a person looks at one and sees what was not on the list.
If you are going to work this way, my recommendation fits in one sentence: take a backup right before every destructive step, not just one general copy at the start. The one that saved the categories was taken deliberately, minutes before the cleanup that broke them. It was not the previous night’s. That difference decides whether you lose one table or the whole day.
Keep reading on IT Rafa
- AI-generated code: the real security cost, with the data from the published studies.
- Your own personal AI assistant: install OpenClaw on Linux
- How to self-host DeepSeek AI on your own server