Keep the URLs the same where you can, then diff the old and new builds page by page: title, description, canonical, robots, H1, structured data and internal link count. Anything that changed should be deliberate. Pages in the old build with no match in the new one need a redirect.

Key takeaways

  • Keeping URLs the same is the safest redesign. Google has separate guidance for site changes that keep URLs and for those that change them.
  • Templates make accidents sitewide. One edit to a head partial can change the canonical on every page.
  • Diff, do not eyeball. Compare facts that search engines read, for every page, and review only the differences.
  • A missing page is a decision. Anything removed needs a redirect to its closest equivalent or a deliberate 404.
  • Check the staging site is not indexable, then check the live site is. The two most common redesign mistakes are opposite ends of the same noindex.

Before you start

  • Crawl or save the old site as static HTML, or keep a copy of the build, so you have something to compare against.
  • Keep URLs. If a URL must change, plan the map with changing URLs without losing rankings.
  • Keep the content the same on purpose, so any ranking change can be traced to the design, not the copy.
  • Keep staging out of the index with a login or noindex, and remember to remove it at launch.

The diff script

Point it at two folders of HTML (for example the old build and the new one). It lists removed and added pages and, for pages in both, which of eight facts changed:

import re, sys, os, html

def facts(path):
    s = open(path, encoding='utf-8').read()
    g = lambda p: (re.search(p, s, re.S | re.I) or [None, ''])[1].strip() if re.search(p, s, re.S | re.I) else ''
    strip = lambda t: html.unescape(re.sub(r'<[^>]+>', '', t)).strip()
    return {
        'title': strip(g(r'<title>(.*?)</title>')),
        'description': g(r'<meta[^>]+name=["\']description["\'][^>]*content=["\']([^"\']*)'),
        'canonical': g(r'<link[^>]+rel=["\']canonical["\'][^>]*href=["\']([^"\']*)'),
        'robots': g(r'<meta[^>]+name=["\']robots["\'][^>]*content=["\']([^"\']*)'),
        'h1': strip(g(r'<h1[^>]*>(.*?)</h1>')),
        'h1_count': str(len(re.findall(r'<h1[\s>]', s))),
        'jsonld_blocks': str(len(re.findall(r'application/ld\+json', s))),
        'internal_links': str(len(re.findall(r'<a[^>]+href=["\'](?!https?:|mailto:|#)', s))),
    }

def pages(root):
    out = {}
    for d, _, fs in os.walk(root):
        if '.git' in d.split(os.sep): continue
        for f in fs:
            if f.endswith('.html'):
                out[os.path.relpath(os.path.join(d, f), root)] = os.path.join(d, f)
    return out

old, new = pages(sys.argv[1]), pages(sys.argv[2])
print('Pages: %d before, %d after' % (len(old), len(new)))
for p in sorted(set(old) - set(new)): print('REMOVED  %s   (needs a redirect or a deliberate 404)' % p)
for p in sorted(set(new) - set(old)): print('ADDED    %s' % p)
changes = {}
for p in sorted(set(old) & set(new)):
    a, b = facts(old[p]), facts(new[p])
    for k in a:
        if a[k] != b[k]:
            changes.setdefault(k, []).append((p, a[k], b[k]))
for k, items in changes.items():
    print('\n%s changed on %d page(s)' % (k, len(items)))
    for p, a, b in items[:4]:
        print('   %s\n      before: %s\n      after:  %s' % (p, a[:90], b[:90]))

A real run on this site

I ran it on an earlier commit of this site and the current one, a worktree checkout of each. It reported 48 pages before, 58 after, ten pages added, none removed. It also reported, for 40 pages, that the canonical had changed, and for 33 that the internal link count had changed. Both were intended: the canonicals moved to clean URLs, and each page gained links to the new guides. The point is that I could confirm they were the only changes, instead of hoping.

What the diff reported against a deliberate change set.
DifferenceCountExpected?
Pages removed0Yes
Pages added10Yes, new guides
Canonical changed40Yes, to clean URLs
Internal link count changed33Yes, new cross-links
Titles, descriptions, H1s, robots, JSON-LD block counts changed0Yes, nothing unintended

At launch

  1. Remove any staging noindex or password, and run the post-deploy checker from the traffic-drop triage on the key URLs.
  2. Submit the sitemap again and spot-check pages with the URL Inspection tool.
  3. Run the pre-launch checklist for everything the diff does not cover.
  4. Watch the Page indexing and Performance reports for the following weeks; Google says to expect temporary fluctuation when a site changes.

What this does not cover

The diff compares markup, not rendering. It reads the HTML as delivered, so content and links that appear only after JavaScript runs are invisible to it. It does not compare visible text, images, page speed or accessibility, and it says nothing about how Google will rate the new design.

Badri Dutta

Software engineer · 8 years technical SEO

Written after using this diff to confirm a sitewide canonical change on this site touched exactly the pages I expected and nothing else.

Full background →