I opened my WooCommerce store one morning and it was in Spanish. All of it. Three hundred and thirty-six translated product pages that I had never written, indexed under my domain, with my content and someone else's links.
This is what that looked like under the hood and how I rolled it back.
The symptom
Search Console started reporting a spike in indexed URLs for the domain. Not a few. Hundreds of new paths like:
/es/producto/some-product-name/ The pages were real. They returned 200. They carried my product titles machine-translated, my images hotlinked, and at the bottom, links to a domain I had never heard of.
How a site gets cloned
Three mechanisms cover almost every case, and they look identical from the outside:
- An affiliate or content plugin with a translation feature. It registers a rewrite that serves translated copies of your own pages, usually under a language prefix, and appends its own tracking.
- A scraper with a fast index. A third party mirrors your pages on their server. Your HTML is not on your site at all; what you see in Search Console is their domain, not yours. Check the URL prefix before panicking.
- A compromised theme or plugin file. Rare, but it produces the same rewrite rules.
Mine was the first: an installed plugin quietly added a multilingual rewrite and my permalinks were being regenerated with a prefix.
The 60-second diagnosis
Ask the site itself which rules are answering those requests:
wp rewrite list --format=csv --path=/var/www/html And ask WordPress whether the prefix corresponds to a real post type, taxonomy or language:
wp post list --post_type=page --fields=ID,post_name --path=/var/www/html | grep -i "es$" If the URLs return 200 but there is no corresponding content, the route is being generated, not stored. That tells you to look at plugins and rewrite rules rather than at your database.
A faster check that requires no shell access: request one of the mirrored URLs with a cache-busting query, and look at the response headers.
curl -sI "https://example.com/es/producto/some-product/" | grep -i -E "x-|server|location" A plugin-generated route usually still emits your caching headers, because the response is generated by your own stack. A scraper's copy does not.
Rolling it back
The order matters, because each step changes what the next one sees:
- Disable the plugin generating the rewrite. Do this first. Removing URLs while the route still exists means they reappear on the next request.
- Flush rewrite rules. In WP-CLI,
wp rewrite flush --hardregenerates permalinks without the prefix. - Purge the page cache. Otherwise your cache keeps serving the translated HTML from stale entries and you will think the rollback failed.
- Verify with curl, not a browser. Browsers hold their own cache. Request ten of the mirrored URLs and confirm 404 or 410.
- Ask Google to remove them. In Search Console, the Removals tool takes a prefix and drops those URLs from results temporarily while the 404s propagate.
for u in $(cat mirrored_urls.txt); do printf "%s " "$(curl -s -o /dev/null -w '%{http_code}' -A 'Mozilla/5.0' "$u")" echo "$u" done You want a clean column of 404s or 410s with no 200s left.
What I would check weekly
A clone does not announce itself. What surfaces it is a change in indexed URL count for a prefix you do not recognise, so watch that number instead of your total. In my case the count went from my real catalogue to roughly double it within a week.
Also check the referrer and outbound link report. Mirrors almost always carry a footer link to the operator's domain, and that is the fastest fingerprint available.
The uncomfortable part
Nothing was hacked in the sense of a stolen password. A feature I installed did exactly what it was configured to do, and the configuration was mine, months earlier, forgotten. The rollback took twenty minutes. Finding it took a week, mostly because I kept checking whether the site was broken for visitors, and it never was.
Originally published on my diary: The Night the Site Spoke Spanish: Waking Up to 336 Mirrors I Never Built