The site you are reading was not always WordPress. It started as a static export from Next.js, sitting in the same folder on the same shared hosting account at Hostinger. When I moved it to WordPress I did what everybody does: I installed WordPress, rebuilt the content, checked that the new pages loaded, and moved on to the next thing.
Two months later, auditing this site in September 2026, I found the entire old site still there. Not archived. Live. Twenty-seven URLs from the old version, returning 200 to anyone who asked, including Googlebot, with their own navigation, their own legal pages in Spanish and their own byline.
Nothing was broken, which is precisely why I had not noticed. This is the part of a migration that the step-by-step guides skip, because it happens after the exciting bit. Here is what was actually on that server, why it survived, and what it takes to close an old site down properly.
What was still on the server
A static export leaves a very particular kind of mess, because every page is a real file. There is no application deciding what exists; the files exist, and the web server serves them.
- An
index.htmlin the web root, plus anindex.txtbeside it, and the same pair inside most folders. - A
/blog/folder holding the old articles, each one a directory with its ownindex.html. - Spanish paths from the first version of the site:
/contacto/,/sobre-nosotros/,/politica-privacidad/,/terminos-condiciones/,/aviso-legal/,/politica-cookies/and a/categoria/tree. - A
/_next/directory of build assets, a/buscar/search page and a404.htmlthat returned status 200, which is its own small disaster. - An old
sitemap.xmlandsitemap-0.xml, listing all of it, cheerfully.
Those pages also carried a byline that did not belong to a real person, and claims about testing that I could not stand behind. That is a separate piece of housekeeping and I have dealt with it on the editorial policy page, but it changed one practical decision here, which I will come back to: a page you would not stand behind should not be redirected to a page you would.
Why a dead site keeps breathing
WordPress routes a request through index.php only when the file system has nothing better to offer. Apache checks for a real file or directory first. If /blog/some-post/index.html exists on disk, that is what gets served, and WordPress is never consulted. The two sites are not fighting. They are politely taking turns, and the old one goes first.
Nothing on the new site linked to any of it, which is exactly what made it invisible to me and perfectly visible to a crawler. Search engines do not need a link when they already have the URL from an old sitemap, and mine was still being advertised. Check the coverage report in Search Console after a migration and you will find out whether you have this problem; the URL inspection tool tells you page by page. I go through the parts of that tool I use in the Search Console guide.
The two files that kept it alive
My robots.txt was still pointing at the old sitemap index, which listed the old sitemap, which listed the twenty-seven old URLs. A closed loop that kept re-announcing a site I thought I had retired.
It had a second problem I did not expect. The file began with a UTF-8 byte order mark, three invisible bytes before the word User-agent, left there by whatever editor wrote it. Depending on what is reading the file, that first line can be misparsed. It is the sort of thing you never see in a browser and find in one second from a terminal:
curl -s https://example.com/robots.txt | od -c | head -1
# should start with: U s e r
# mine started with: 357 273 277 U s e r
The replacement is plain, saved without a BOM, and points at the sitemap the current site actually generates:
User-agent: *
Disallow: /wp-admin/
Allow: /wp-admin/admin-ajax.php
Sitemap: https://example.com/wp-sitemap.xml
One deliberate omission: I did not block the old paths in robots.txt. Blocking a URL stops it being crawled, which stops the crawler from ever seeing the redirect or the 410 you just set up. If you want a page gone, let it be crawled and give it an answer.
Giving every old URL a decision
I exported the list of old URLs from the old sitemap, put it in a spreadsheet, and forced myself to write one of three words next to each: redirect, gone, or keep. No blanks. It took under an hour and it is the only part of this I would call a method.
| Old URL | Decision | Why |
|---|---|---|
/sobre-nosotros/ | 301 to /about/ | Same purpose, new language, real equivalent |
/contacto/ | 301 to /contact/ | Same |
/blog/best-vps-hosting-providers-2026/ | 301 to the current VPS guide | The subject survived, the article did not |
/categoria/saas-tools/ | 301 to the guides index | The category is gone, but someone who wanted a list of articles still gets one |
/_next/, /buscar/, 404.html | 410 | Build artefacts and machinery, never content |
/sitemap-0.xml | 410 | It was the thing keeping the list alive |
The rule I ended up with: a 301 is a promise that the new page answers the same question. Where that was true I redirected. Where it was not, I used 410, which says the page is gone on purpose, rather than 404, which says it might be a mistake. Google treats them similarly in the long run, but 410 is the honest signal and it tends to be acted on faster.
Redirecting everything to the home page is the popular shortcut and it is worse than a 410. It sends a reader who wanted a specific article to a page that does not contain it, and it tells the crawler nothing true. Google’s own documentation on redirects is worth ten minutes if you are about to do this at scale.
Where the rules have to live
On Apache, in .htaccess, in your own block, placed before # BEGIN WordPress and outside it. This matters: WordPress rewrites the contents of its own block whenever you save permalink settings, so anything you put in there disappears one day without explanation.
# BEGIN MY-REDIRECTS
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
# Equivalent page exists: 301
RewriteRule ^sobre-nosotros(?:/(?:index\.(?:html|txt))?)?$ https://example.com/about/ [R=301,L]
# Nothing replaces it: 410 Gone
RewriteRule ^_next/ - [G,L]
RewriteRule ^sitemap-0\.xml$ - [G,L]
</IfModule>
# END MY-REDIRECTS
# BEGIN WordPress
# ... do not touch anything below this line ...
Two details that cost me time. The old export served both /page/ and /page/index.txt, so every rule needs to match the index.html and index.txt variants or you will leave half of each page behind. And take a copy of the existing .htaccess before you touch it. A stray character in that file does not produce a warning, it produces a 500 on the whole site.
The folder that has to move before the rule can work
Redirect rules do not help while the old files are still winning the race. I wanted /blog/ to become the posts index of the new site, and it could not, because there was a real blog directory with a real index.html in it.
So the old folders get moved out of the web root, not deleted: compress them, download the archive, then move the originals to a folder that sits beside public_html rather than inside it. Moving is reversible and deleting is not, and I have never once regretted keeping the archive. What stays put, obviously, is everything WordPress owns: wp-admin, wp-content, wp-includes, the wp-*.php files, index.php, .htaccess, ads.txt and robots.txt.
This is also the moment to take a full backup from the panel, before any of the moving starts. My habit of taking a local copy before a big change is the only reason this was a calm afternoon rather than a tense one; the routine is in my backup routine.
Checking it, rather than hoping
Every URL on the list gets tested, and the test takes seconds. What I want to see is the status code and, for redirects, where it lands:
while read -r url; do
printf '%s -> %s\n' "$url" "$(curl -s -o /dev/null -w '%{http_code} %{redirect_url}' "$url")"
done < old-urls.txt
Three things I look for. Every retired URL answers 301 or 410 and none answers 200. No redirect points at another redirect, because chains get truncated and they are slow. And, most important, none of the URLs that are supposed to stay has been caught by a rule: a regular expression written at speed will happily match a page you still publish. Then I submit the correct sitemap in Search Console with its full URL, delete the old sitemaps from the property, and ask for indexing on the pages that changed.
The list I use now
- Export every URL of the old site before touching anything, from its sitemap, its log files, or by crawling it.
- Write redirect, gone or keep next to each one. No blanks.
- Full backup from the panel, plus a local copy, before the first file moves.
- Move old files out of the web root; compress and download first, never delete.
- Add the redirect block above
# BEGIN WordPress, in a block of your own. - Replace
robots.txt, with no BOM, pointing at the new sitemap, and block nothing you want de-indexed. - Test every old URL for status and destination, and test a sample of the URLs that stayed.
- Submit the new sitemap in Search Console with its full address, remove the old ones, request indexing for what changed.
What I would change next time
I would do the URL inventory on the first day rather than the last. Everything above is easy; it is only unpleasant because I did it late, when I had forgotten which old article corresponded to which new one and had to work it out from the titles.
I would also stop treating “the new site works” as the definition of done. A migration is finished when every old address has an answer and the old sitemap is gone, not when the new home page loads. If your domain also changed, or you are pointing a domain at a different server entirely, the DNS half of the job is a separate piece of work and I have written it up in pointing a domain at a host without downtime.
And I would be less romantic about redirects. Redirecting a weak old article to a good new one does not transfer any credibility; it just puts a reader in front of something they did not ask for. Several of my old pages got a 410 because the honest answer was that the page should never have existed. The broader technical clean-up around all of this, including canonicals and the punycode domain this site sits on, is in retiring an old site on the same domain. And if you are choosing where to move to rather than how, the checklist I run before putting a site on a host is the piece that comes first.
