Skip to content
All writing

Rebuild the site, keep the URLs

Passion, 31 March 2026

A website rebuild is one of the few projects where you can do everything right and still be worse off. The design is better, the pages load faster, the client is pleased, and eight weeks later the enquiries are down a third because a few hundred addresses that used to rank now return a 404. Nobody connects the two events, because the launch got its congratulations in week one and the traffic left slowly in weeks three to nine. We migrated an accountancy practice off a legacy content system recently, about eighty-five articles and twelve service pages, some of it live for years. This is the checklist we used.

Inventory first, design second

Before anyone opens a design tool, build a list of every URL the old site can serve. Combine four sources, because each one misses something.

The old system's own database. A query against the node or post table gives you the definitive list of published content, including things nobody has linked to in years. This beats a crawl because a crawler only finds what is linked.

A crawl anyway. It picks up files, redirects already in place, taxonomy pages, paginated archives and the odd orphan that the database does not classify as content.

Search Console, exported. Sixteen months of pages by impressions and clicks. This is the honest ranking of what matters, and it is the only source that tells you which of those old pages anyone actually arrives on.

Server logs. Specifically, requests that already 404, and requests for files: PDFs, images, downloads. Those accumulate links from other websites and get forgotten in every migration.

Union the four, remove duplicates, and sort by impressions rather than by folder. The top fifty rows are the ones a mistake will be expensive on.

Three fates, decided per URL

Every old address gets exactly one of three outcomes, written down before the build.

Old URL Fate New URL Status
/services/vat-and-payroll keep the path same 200
/node/482 redirect /articles/making-tax-digital-explained 301
/taxonomy/term/17 redirect to nearest /articles?tag=payroll 301
/sites/default/files/guide.pdf keep the file path same 200
/user/login delete none 410

Four rules make the table work. Use 301, not 302, because a temporary redirect asks search engines to keep the old address in the index. Allow one hop only: chains of two and three redirects appear whenever a migration happens on top of an earlier migration, and they waste crawl budget and lose a little of everything each time. Redirect to the closest real equivalent, never to the homepage in bulk, since a mass homepage redirect is treated as a soft 404 and the reader gets nothing useful either. And prefer keeping a path over redirecting it, even when the new path would be prettier. Pretty URLs are worth roughly nothing next to URLs that already have links pointing at them.

Keep the old identifiers in the new database

The tempting implementation is a redirect file with several hundred lines in it. It works on day one and rots quietly, because the moment somebody edits a slug in the new admin area the file is wrong and nothing tells you.

Better: carry the old identifier as a column on the content row and resolve redirects with a lookup.

alter table articles add column legacy_node_id integer;
create unique index articles_legacy_node on articles(legacy_node_id);
GET /node/:id
  -> select slug from articles where legacy_node_id = :id
  -> found:     301 to /articles/{slug}
  -> not found: 404

Three benefits. The mapping stays correct when a slug changes later, because it is derived rather than written down. There is one code path to test instead of one line per URL to eyeball. And you can answer the question "what was this page before" from the data, which you will want during the first month when someone reports a link that no longer works.

The same trick applies to menu identifiers and to any other numeric key the old system leaked into public URLs.

What breaks that is not a URL

Preserving addresses is necessary and not sufficient. The rest of the list, roughly in order of how often we see it:

Canonical tags pointing at the staging domain. The single most common launch bug, and it tells search engines the real site is a copy of a site they cannot reach.

A robots.txt carried over from staging with Disallow all in it, or a noindex left in a shared template.

Titles and headings rewritten wholesale during the redesign. A page that ranks for a phrase because its heading contains that phrase will stop ranking for it. Redesign the layout freely; change the words on your best fifty pages deliberately and one at a time.

Internal links still pointing at old paths. Everything works, because the redirects catch them, which is exactly why nobody notices. Rewrite internal links to their new targets and keep the redirects for the outside world.

Trailing slashes and letter case. /Services/ and /services are two different addresses to a crawler. Pick one form, enforce it with a redirect, and make sure your sitemap agrees with the choice.

The sitemap itself. Generate it from the route list and the content table, at build time or on request. A hand-maintained sitemap is a lie with a timestamp on it.

Structured data quietly dropped in the rebuild, because it lived in a template nobody ported.

The part we would do differently

Being straight about a decision we are not fully happy with. That site is a client-rendered single page application: the content comes out of the database in the browser and the page metadata is set after the app boots. It is indexed, and search engines do run JavaScript, but you have handed the crawler an extra job that buys the reader nothing. Prerendering the article and service pages to HTML at build time would have cost very little and removed an entire category of anxiety about whether a page is being seen.

If you are migrating a content site that already ranks, the safe default is HTML in the response. Choose the client-rendered version when the application behaviour genuinely needs it, not because the rest of the project is built that way.

What to check, and when

Day zero, before you switch DNS. Crawl the staging site using your old URL inventory as the seed list. Every entry must return 200, or one 301 to a 200. Check robots.txt. Check the canonical tag on three pages of each type. Read the rendered HTML of one article with JavaScript disabled and see what is actually there.

Day one. Repeat the same crawl against the live site. Confirm the sitemap is reachable and contains the new URLs, then submit it. Start watching the 404 log.

Week one. Read the 404 log daily. Real people and real crawlers will find the addresses your inventory missed, and they will find them within hours. Anything with traffic behind it gets a redirect the same day.

Weeks two to four. Search Console coverage. You want "Not found" and "Page with redirect" counts falling, and you want your important pages out of "Crawled, currently not indexed".

Months two and three. Compare impressions per URL, old against new, for the fifty pages you cared about. Not total sessions. Sessions move for a dozen unrelated reasons and will let you argue yourself into any conclusion you like.

Expect a dip. Two to six weeks of wobble while the index catches up is normal even on a clean migration. A dip that is still deepening after six weeks is not the migration settling down. It is a bug you have not found yet.

None of this appears in the design, and the mark of doing it well is that nobody outside the project ever notices the site changed.

The site this came from is Taxcare Accountancy.