Traffic Dropped After a Website Redesign or Migration: How to Diagnose and Recover
Traffic fell after the new site launched. First prove the drop is real, not broken tracking, then find which pages and queries lost out and why.
Google is indexing the wrong version of your pages: parameters, staging copies, attachment pages or archives. See which canonical Google chose and steer it back.
In Search Console you see Duplicate without user-selected canonical or Duplicate, Google chose different canonical than user, or you notice Google showing a strange version of your page: one with ?utm_source= on the end, an image attachment page, a tag archive, or worse, staging.example.com. Your real page is missing or ranking below a copy of itself.
This is canonicalization. When Google finds several URLs with the same or very similar content, it groups them and picks one as the canonical, the version it indexes and shows. The others become duplicates. Google decides using signals you send (redirects, canonical tags, internal links, sitemaps) and signals you do not control. Duplicate content is not a penalty by itself, but when Google picks the wrong URL, your rankings, links and reporting are split across copies. This guide is for site owners and designers who need to find every source of duplicates on a WordPress site and send consistent signals.
Google weighs several signals when choosing a canonical. Its documentation describes redirects as a strong signal, rel="canonical" as a strong signal too, and sitemap inclusion as a weaker one. None is an absolute command. If your signals disagree, for example the canonical says one URL but every internal link points to another, Google may choose for itself.
| Duplicate source | Example | Best tool |
|---|---|---|
| Protocol and host | http://, www. and bare domain versions |
301 redirect to one version (FG-39) |
| Trailing slash and case | /about and /about/, /About/ |
301 to the permalink form |
| Tracking parameters | ?utm_source=, ?fbclid=, ?gclid= |
Self-referencing canonical |
| Sorting and filters | ?orderby=price, ?filter_color=red |
Canonical, fewer crawlable links |
| Comment reply links | ?replytocom= |
Canonical; SEO plugins can strip it |
| Staging and dev copies | staging.example.com |
Password, noindex, Removals tool |
| Attachment pages | /my-post/photo-name/ |
Redirect attachment pages to the file |
| Thin archives | Tags with one post each, author archives on a one-author site | noindex or remove |
| Print or alternate versions | ?print=1, /print/ |
Canonical to the main page |
| Files | A PDF that duplicates an HTML page | HTTP Link header canonical |
curl runs on your own computer; macOS and Linux include it, and on Windows you use curl.exe in PowerShell or WSL. wp commands run in an SSH terminal in the folder that holds wp-config.php; some hosts call it wp-cli. Blocks starting with <Files or location are server config you edit, not commands.Paste a page you care about into the URL Inspection bar in Search Console. In the indexed result, open Page indexing and compare two lines:
If they match, Google agrees with you. If Google selected another URL, inspect that URL too and look at how it differs. The Google-selected canonical comes from Google’s last indexing of the page, so after a fix, the live test confirms your declared canonical but the selected one updates only after Google reprocesses the page.
Then check what the page really outputs:
curl -s https://example.com/PAGE_PATH/ | grep -io '<link[^>]*rel=.canonical[^>]*>'
curl -sI https://example.com/PAGE_PATH/ | grep -i '^link:'
Run these on your computer. Replace PAGE_PATH with the page path. The first finds canonical tags in the HTML (the . matches either quote style), the second finds a canonical sent as an HTTP header. Good output is exactly one canonical with the page’s own full URL. Bad output is two canonical tags (often a theme and a plugin both printing one), a canonical with a different host such as staging, an http:// canonical on an https site, or every page pointing at the homepage.
Every page should answer at exactly one address. Redirecting the http and www variants to your chosen version in a single hop is covered in FG-39. Check the trailing slash behavior here:
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' https://example.com/PAGE_SLUG
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' https://example.com/PAGE_SLUG/
Run these on your computer. Replace PAGE_SLUG with a page slug. -o /dev/null discards the body (use -o NUL in Windows PowerShell) and -w prints the status and redirect target. Good output is one version returning 200 and the other returning 301 to it. WordPress normally does this itself, matching the trailing slash in your permalink structure, through its redirect_canonical feature. Bad output is 200 for both, which usually means a caching layer or server rule is answering before WordPress, or a plugin has disabled canonical redirects.
Parameters create endless duplicates. Tracking parameters from ads, email and social links, sorting and filtering in shops, and ?replytocom= on comment reply links all produce new URLs with the same content.
?utm_source=test added: the canonical should be the clean URL.?utm_ tags send a conflicting signal and break your own analytics attribution.?orderby=) and layered navigation filters can multiply URLs. Canonicals to the unfiltered category help, and filter plugins often offer settings to stop filter links being crawlable. If thousands of filter URLs fill the Page indexing report, blocking the parameter pattern in robots.txt reduces crawl waste, but blocked URLs cannot pass their canonical signal, so only do it for patterns that never deserve indexing.?replytocom variables; turn it on if those URLs appear in your reports.Search Console’s old URL Parameters tool was retired in 2022, so there is no setting in Google to tell it to ignore a parameter. Your own site has to send the signal.
A staging or dev copy that is open to the world will eventually be found. Once indexed, it competes with your live site and can even be chosen as canonical. Confirm with a site:STAGING_HOSTNAME search, replacing STAGING_HOSTNAME with the staging domain. Remove it with three layers, in this order:
https://STAGING_HOSTNAME/. Google applies a prefix removal to the http and https versions alike, so one request covers both. This hides the URLs from results quickly, but only temporarily, for about six months.X-Robots-Tag: noindex header on the staging host. Anything Google can still reach carries a noindex.Do not block staging with robots.txt instead. Google would stop crawling, never see the noindex, and could keep the URLs indexed from links. Also check that no live page links to staging and that the live site’s canonicals do not point there:
wp db search 'STAGING_HOSTNAME' --all-tables --stats
Run this over SSH in the live site’s folder. Replace STAGING_HOSTNAME with the staging domain. --all-tables searches every table, including plugin tables, and --stats prints a match count per table. Good output shows zero matches. Matches in post content, options or SEO plugin tables need a wp search-replace run, with --dry-run first and a backup before the real run.
Every uploaded image or file can have its own page with a URL like /my-post/photo-name/, holding little more than the image. Since WordPress 6.4, new installs redirect attachment pages to the file itself. Sites created earlier keep attachment pages unless something changes that. Check:
wp option get wp_attachment_pages_enabled
Run this over SSH in the site folder. Output 0 means attachment pages redirect to the file. Output 1 means they are live pages. An error saying the option does not exist usually means WordPress is older than 6.4, where attachment pages are always live; update WordPress first. To turn them off, run wp option update wp_attachment_pages_enabled 0, or use the equivalent redirect option in Yoast SEO or Rank Math. Confirm by visiting an old attachment URL: it should redirect to the file.
Print plugins or themes that create ?print=1 or /print/ URLs should output a canonical pointing to the main page. Most sites today use a print stylesheet instead, which creates no extra URL at all.
Archives list excerpts of posts that exist elsewhere. A category with real depth can be useful. Hundreds of tags with one or two posts each are thin duplicates. Prune or merge unused tags, and use your SEO plugin to set noindex on tag archives (or author and date archives on a single author site) if they add nothing. Keep them crawlable so Google can see the noindex.
Page 2, 3 and onward of a blog or category list should each have a canonical pointing at themselves, not at page 1. Pointing them at page 1 tells Google they are duplicates, which can hide the posts linked only from deeper pages. Google no longer uses rel="next" and rel="prev" as an indexing signal, so focus on clean links and self-referencing canonicals.
A canonical is a hint, so it only works when the rest of your site agrees with it. The rules that matter:
https:// and the host.<head>. A canonical in the body, or two conflicting ones, may be ignored.A PDF cannot contain an HTML tag, so if a PDF duplicates a web page, send the canonical as a Link header. For Apache or LiteSpeed, in .htaccess in the folder that holds the file:
<Files "PDF_FILE.pdf">
Header add Link "<https://example.com/CANONICAL_PAGE/>; rel=\"canonical\""
</Files>
For Nginx, in the site’s server block:
location = /wp-content/uploads/YYYY/MM/PDF_FILE.pdf {
add_header Link '<https://example.com/CANONICAL_PAGE/>; rel="canonical"';
}
These are config edits, not commands. Replace PDF_FILE.pdf with the file name, YYYY/MM with its upload folder, and CANONICAL_PAGE with the HTML page that should rank. The Apache version needs mod_headers. Test with curl -sI https://example.com/wp-content/uploads/YYYY/MM/PDF_FILE.pdf on your computer: good output includes a link: line with your page URL and rel="canonical". No such line means the rule is not applied (wrong folder, module missing, or a CDN serving the file from cache). Use the header or the HTML tag on a page, not both.
Find posts with a manual canonical set:
wp db query "SELECT post_id, meta_key, meta_value FROM $(wp db prefix)postmeta WHERE meta_key IN ('_yoast_wpseo_canonical','rank_math_canonical_url') AND meta_value <> ''"
Run this over SSH in the site folder. There are no placeholders; $(wp db prefix) inserts your table prefix. The two meta keys are where Yoast SEO and Rank Math store manual canonicals; if you use another plugin, check its documentation for the key name. Good output is no rows, or only canonicals you set deliberately. Rows pointing at staging, an old domain or unrelated pages need clearing in the post editor.
Spot checks miss template-level problems. This loop reads your sitemap and reports every URL whose canonical is not itself:
curl -s https://example.com/SITEMAP_FILE.xml | grep -o '<loc>[^<]*' | sed 's/<loc>//' | while read -r url; do
canon=$(curl -s "$url" | grep -io '<link[^>]*rel=.canonical[^>]*>' | grep -o 'href="[^"]*"' | head -1 | cut -d'"' -f2)
[ "$canon" != "$url" ] && echo "MISMATCH $url -> ${canon:-NONE}"
done
Run this on your computer in macOS, Linux or WSL. Replace SITEMAP_FILE.xml with a single child sitemap, for example wp-sitemap-posts-post-1.xml for WordPress core sitemaps or post-sitemap.xml for Yoast SEO; a sitemap index only lists other sitemaps. The first line extracts each <loc> URL, the inner curl fetches each page, and the comparison prints only mismatches. Good output is nothing at all. MISMATCH ... -> NONE means a page has no canonical; MISMATCH with another URL means a template, plugin setting or manual field is sending Google elsewhere. Mismatches that differ only in a trailing slash or http versus https are still worth fixing, because they contradict your sitemap. The loop fetches pages one at a time, so on large sites run it per sitemap rather than all at once.
Open a ticket if a canonical Link header appears that you did not set, if a staging or temporary hostname provided by your host is indexed and you cannot password protect it yourself, or if both trailing slash versions return 200 because of a server cache. Include:
curl -sI output for each, with any CDN ray or request IDs.site: search for it.Our diagnostic tools can show the redirects and headers a URL returns, including Link headers. If the problem is bigger than duplicates and the site is down, start at the Emergency Room.
Did this fix it?
Still stuck? Start again from what you can see in the Emergency Room.
Still stuck?
Print a free one-page checklist to keep, or open the Emergency Room if the symptom does not match.
Get the free checklistsKeep reading
Search the field manual
Or jump straight to a symptom
Press / to search from any page, Esc to close