SYSTEM DEBRIEF

Refreshing 102 Stale Travel Pages with an AI Harness and WP-CLI

Debrief: 5
278 pages of travel guidance had quietly gone stale — some of it stating the opposite of current law. Here is how 102 of them were researched, corrected and deployed in days rather than weeks, and the four things that went wrong along the way.
Futuristic airport scene representing travel info.

SouthAmerica.travel has 278 pages of practical travel guidance sitting under /{destination}/travel-info/ — visa rules, money and currency, safety, languages, packing lists, across 17 destinations. It is the kind of content that quietly decays. Nobody notices when a visa fee changes, and the page keeps confidently telling people the old number.

When I actually read them, some pages weren’t stale so much as wrong in the opposite direction. The Bolivia visa page told American travelers they needed a US$160 visa. Bolivia had dropped that requirement on 1 December 2025. The Colombia page told people to sort out extensions at “DAS” offices — an agency dissolved in 2011. That’s not tone drift. That’s a page actively giving bad instructions to someone about to book a trip. The client knew they needed a content update, but didn’t know how to even tackle this massive project.

This is the record of how 102 of those pages got researched, corrected, and deployed — 68 of them in three consecutive days — using Claude Code over SSH and WP-CLI. It covers the parts that were genuinely hard, which turned out not to be the writing.

Why it never got done by hand

Doing one of these properly is not a ten-minute job. You have to check the current rule against a primary source — an embassy notice, a State Department advisory, a migration agency — then rewrite without flattening the existing voice, keep the specifics that were already good, restructure the headings, set the SEO fields, back up what you’re about to overwrite, deploy it, clear the caches, and confirm on the live URL that the thing you wrote is the thing being served.

Call it 1.5 to 3 hours a page. That’s an estimate, not a measurement — but at that rate the 68-page block is four to six working weeks. That is exactly why it had never been done. There is no version of this where a small agency bills a client for six weeks of checking whether Paraguay still charges a reciprocity fee.

It took three days. The rest of this post is the how, including the parts that bit.

The first job wasn’t writing. It was finding where the words live.

The site runs WordPress with Elementor and JetEngine on Kinsta, and it has the trait that stops anyone approaching this naively: on builds like these, the visible body of a page often isn’t in post_content at all. It lives in custom fields, and the post’s own content column can read as literally zero words.

That has a side effect worth knowing even if you never touch this particular site: any audit that measures “thin content” by post_content length will report the whole section as empty when it plainly isn’t. If a site reports hundreds of zero-word pages that visibly have words on them, this is usually why.

So the first pass wasn’t editorial at all. The harness read the site’s structure and proposed a map — this field holds the body people actually read, that one only feeds a summary, this term selects the template, those fields build the live URLs, and here is what I believe is safe to touch. Then I checked it against what I know about the build, and corrected it where it was wrong.

That review is not a formality. Some of those fields are load-bearing in ways nothing in the data tells you. One turned out to control both which template renders the page and which fields are editable at all. Another set silently generates the live URLs. A confident script treating them as ordinary content would have broken pages that looked perfectly healthy in the database.

This is the part that gets skipped, and it is the whole method. Not “AI, go update my website.” Instead: the harness reads the system and proposes what it thinks is safe to touch; the person who actually knows the site confirms or corrects that; then one page gets done end to end as a pilot — written, deployed, caches cleared, verified on the public URL — and only once that survives does anything scale. Every gotcha in this post surfaced on page one or page five. None surfaced on page fifty, because by then the map was right.

A page can have more than one edit surface

The visa pages carried comparison tables that weren’t really part of the page. They lived in a separate table plugin and were pulled in by a shortcode. Rewrite the page body and the table still sits underneath it, quietly stating the old rule — two halves of the same page contradicting each other, on a page about entry requirements.

Two lessons came out of that, and neither is specific to this site. Inventory every surface that renders into a page before you start, not after you spot the mismatch. And treat a shared component as shared: some of those tables fed several pages at once, so every edit had a blast radius, and “what else consumes this?” had to be answered before anything was touched.

The tables also didn’t share a common shape — some carried columns the others didn’t. A script assuming one schema across all of them would have written the right values into the wrong columns and corrupted rows without throwing a single error. That is exactly the class of problem a human spot-check catches and a confident bulk operation does not.

Writing without corrupting the bytes

Early attempts mangled content — shell quoting plus accented characters (á, í, ñ) is a reliable way to destroy a paragraph. The loop that worked was boring and never failed again: stage the exact bytes in a local file, base64 -w0 it, decode server-side, and write from the file.

wp post meta update <post-id> <field> "$(cat /tmp/body.html)"

A related gotcha, learned the annoying way: don’t try to batch several pages into a single remote shell loop. An unquoted | used as a field delimiter gets parsed as a pipe on the remote host. Loop locally, open one connection per page.

Backups, and why “just roll the site back” isn’t the answer

WordPress revisions don’t version custom fields. Edit one on a published page and it’s live instantly, with no per-page history to step back through.

The host can roll the whole site back, and that’s a genuine safety net worth having. But it’s a blunt one. Restoring yesterday’s site to undo one bad paragraph also undoes everything else that happened in between — other people’s edits, form submissions, enquiries. On a live site taking booking requests, “we lost a day” is a considerably worse outcome than the thing you were trying to fix.

So the rule was to back up the exact fields being overwritten, page by page, immediately before each write. Reverting one page then costs one command, and the site-wide rollback stays where it belongs — as the thing you keep and never need.

Partway through we moved from draft-first to deploying straight to live, with me reviewing the published page and calling a revert if needed. That was a deliberate trade of review latency for speed, and it was only acceptable after the pilot had proved the loop end to end. I’d make the same call again — but not on day one, and not without the per-page backups underneath it.

Cache purge ordering, which cost us real time

Switching to “we” here, because this one took both of us — the harness could see the database and I could see the browser, and for a while those disagreed.

After writing Bolivia’s visa table, the live page still showed the old text. The obvious conclusion was that the write had failed. It hadn’t. The correct order is:

# finish ALL writes first, then:
wp transient delete --all      # MUST be first
wp cache flush                 # object cache
wp kinsta cache purge --all    # edge

Purge the edge before clearing transients and a request races in and re-caches the stale output. The failure is indistinguishable from a failed write, and the instinct — re-edit the page — means writing correct data on top of correct data while the real problem sits in the cache layer.

The rule that came out of it: confirm on the public URL with a browser user-agent, and require x-kinsta-cache: MISS and the new strings present and the old strings gone. A HIT that still serves old strings is an edge-cache problem, not a failed write. Purge again; don’t re-edit.

What the fact-check actually found

All verified against primary sources in June 2026, and dated here because this is exactly the kind of thing that rots:

  • Bolivia dropped the US visa requirement effective 1 December 2025. The page still described a US$160 visa, paid in cash, on arrival.
  • Paraguay grants visa-free tourism to US, Canadian, Australian and NZ passport holders, currently through 13 August 2027. The page described required visas and reciprocity fees of US$160/$150/$135. It also contradicted itself — stating in two places that the same nationalities both did and did not need a visa.
  • Chile abolished the Australian reciprocity fee in September 2025.
  • Ecuador’s safety page said “violent crime is very low.” That was false — coastal provinces carried Level 3 and Level 4 advisories at the time of writing, even though the areas SouthAmerica.travel actually sells (Quito, the Andes, Galápagos) were outside them.
  • Every safety page pointed at a dead “Smart Traveler App on iTunes” link and a defunct government Twitter handle.

And the one I’d keep if I could only keep one. The Antarctica languages page said this:

“The most commonly spoken language of Antarctica is Russian, which happens to be the official language of Bellingsgauzenia, New Devon, and Ognia.”

Bellingsgauzenia, Ognia and New Devon are fictional Antarctic nations from a worldbuilding project. None of them exist. Antarctica has no official language. That paragraph had been live and indexable, and what made it dangerous is that the sentences around it were fine — the page opened with a perfectly accurate line about there being no permanent residents, so the invention blended in.

That page is the whole argument for this way of working. It was fluent, confident, well-formed and completely made up — which is precisely what an unverified generative pass produces. No date-based audit flags it. No tone-based audit flags it. Only checking claims against the real world does. The value of the harness here was verification, not generation. Similar story on the Peru money page, which had broken currency-converter markup baked into the text quoting prices in EEK — the Estonian kroon, a currency withdrawn in 2011.

What I got wrong

I shipped business claims that couldn’t be verified. Five money pages went live describing what tour packages include — meals, specifically. That is not a fact-checkable public source; it’s the client’s commercial reality, and it got asserted anyway. The client corrected it: breakfasts are typically included unless noted, while lunches and dinners are generally only included on cruises and at many lodges. The five pages were corrected and redeployed the same day.

The tell is what makes it instructive. The six cruise and lodge money pages already stated meals-included and matched reality — so they were left alone. The invented claim appeared only where the source page was silent. “Verify against a source” has to include “and when the only possible source is the client, go ask the client.”

I shipped five pages as one unreadable wall of text. Custom fields like these are echoed raw — they never pass through WordPress’s the_content filter chain, so wpautop never runs and blank lines between paragraphs are just whitespace. You have to write explicit <p> tags. I verified those pages the way I’d taught myself to — by reading back the stored field — and the field was perfect. The rendered page was a single giant paragraph. I only found out because I opened the live page and looked at it.

Those two verification lessons are exact mirrors of each other, and you need both:

  • For content correctness, check the field you wrote — not the rendered page. (A grep on rendered HTML once “found” a wrong country name three times; all three were navigation-menu links.)
  • For rendering correctness, check the rendered page — not the field. The stored value will happily tell you everything is fine.

I reported project state from my own notes instead of the database. Late in the project I stated that one batch of pages had shipped without SEO title tags. I hadn’t checked; I’d read it in my own project notes, written weeks earlier as a reasonable guess. It was wrong — one WP-CLI loop would have shown me in under a minute — and by the time it was caught it had propagated through three later working sessions.

The rule I took from that: a system model is authoritative about structure — field names, template IDs, what depends on what — and unreliable about state, which changes every time anyone deploys. Read the model to know where to look. Read the database to know what’s there. I fact-checked seventeen governments and then didn’t fact-check myself.

Did it work?

Roughly 170 travel-info pages were never touched, which gives a built-in control group — useful, because the site had relaunched in May 2026 and was still recovering, so everything was rising anyway.

I’m reporting clicks rather than rankings deliberately. Impressions and average position on this estate were badly contaminated — in one cohort, 84% of the baseline impressions came from two queries that generated zero clicks between them. Clicks are immune to that, because a junk impression produces no click by definition. Google Search Console, comparing the month before the refresh to a month roughly two months after:

Page groupClicks beforeClicks afterChange
Safety660+900%
Money82327+299%
Visa138396+187%
Packing1025+150%
Languages199246+24%
Untouched (control)654928+42%
Google Search Console, clicks. Control group = travel-info pages that were never edited.

The control grew 42%, so roughly that much is just the site recovering. The pages that corrected hard facts beat it by a wide margin. Two honest caveats: safety and packing are percentages off tiny bases — six clicks and ten clicks — so don’t read those as tenfold businesses. And the languages pages, at +24%, underperformed the control. I’d rather say that than pretend every cohort won.

One more negative worth publishing, because the opposite gets sold a lot: this did not lift the commercial pages. The theory that refreshing supporting content under /{destination}/ builds topical authority and pulls up the destination hub is not supported here. Two months on, those hubs are flat to slightly worse on their head terms. I think that’s because the hub problem is structural rather than topical — the individual tour pages are surfaced through client-side listing grids whose links aren’t in the server-rendered HTML, so there’s no crawlable path to them — and content freshness on sibling pages doesn’t fix a crawl problem. Anyone selling topical authority as a fix for hub rankings should have to show a table like this one.

The part that answers “isn’t this just AI content?”

It’s the obvious objection and it deserves a real answer rather than a defensive one.

My honest framing at the time: if unreviewed AI content is −1 and genuine expert content is +2, this work moved these pages from −1 to about +1. Correcting a visa fee that reversed eight months ago is real value, but it doesn’t make the page expert. What makes it expert is a person who actually knows the destination standing behind it.

So we built that layer, and the honest description of the finished process is this: AI-assisted research and drafting, then reviewed and adjusted by the business’s resident expert on that specific destination. Not a blanket sign-off in the site footer. The person who actually sells and travels Patagonia reviews the Patagonia pages; someone else owns Brazil. Where a reviewer changed something, the change stuck — the client’s correction on what tour packages include, in the section above, is exactly that loop working.

The plumbing for it had to be built from nothing. No author or reviewer relationship existed anywhere on the destination or travel content, and the team records — job titles, bios, photos, profiles — emitted no structured Person data at all, so search engines were seeing two disconnected versions of the same human. We added canonical Person entities, a reviewedBy relationship on each page, and a visible byline that reads from the same source as the machine-readable version, so the two can never drift apart. Every destination is mapped to its named reviewer.

The harness did the research, the corrections and the deployment. A named human expert (Hi Juergen!) is attached to the result, publicly, in both the markup and on the page. That’s the split I’d defend: the machine is very good at checking seventeen governments’ entry requirements in an afternoon and very bad at knowing whether you should be selling a Level 3 destination at all. When the Venezuela safety page came up, the harness flagged that question rather than quietly answering it — the page was rewritten as an honest advisory with a softened call to action, and the commercial decision went to the humans.

What I’d tell someone starting this

  • Map the system before you write anything — and have a human confirm the map. On a custom-field build the body is rarely in post_content, and a first write into the wrong field renders nowhere. The harness can propose what’s safe to touch; someone who knows the build has to sign it off, because the dangerous fields don’t look dangerous in the data.
  • Pilot on one page, end to end, including the cache purge and the public verification. Every gotcha in this post surfaced on page one or page five, not page fifty.
  • Find every surface that renders into the page. Content pulled in from elsewhere will happily keep contradicting the body you just rewrote.
  • Back up the exact fields you’re about to overwrite. Custom fields have no revision history, and a host-level rollback is far too blunt to use for one page.
  • Verify twice, in both directions — the stored field for content, the rendered page for markup.
  • Put a named expert behind the output. AI-assisted drafting plus review by the person who genuinely knows that subject is a different product from unreviewed generated content — and it’s the half that makes the result worth publishing.
  • The speed comes from the map, not the model. The reason this took three days instead of six weeks isn’t that an AI writes fast. It’s that it had a confirmed working model of the site — what holds the content, what’s load-bearing, what breaks what — so it wasn’t rediscovering the system on every page. Generation was never the bottleneck. Knowing where to put the words safely was.


Debriefs are drafted from live working-session context by the engineering harness that did the work, then reviewed, verified, and published by me.

Observed: Jun 12, 2026
Verified: Aug 18, 2026
Debrief: 5
Status: Current
Type: Implementation
Systems:
WordPress
GSC
Tech Stack:
Elementor
JetEngine
Claude Code
WP-CLI
TablePress
Rank Math
Kinsta
Problem Domains:
Data Integrity
SEO
Content Systems