Skip to content
  • FG-81
  • 12 min read
  • Last checked November 8, 2026

How to Fix Duplicate Content and Canonical Problems on a WordPress Site

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.

Run the health check

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.

What is happening

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

Before you start

  • Access you need: Search Console (a Domain property shows all host and protocol variants together), the WordPress dashboard, your SEO plugin settings, and SSH or SFTP for server config.
  • Backup: back up files and database before bulk redirects, server rule changes or plugin setting changes.
  • How to run commands: 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.

Step 1: See which canonical Google chose

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:

  • User-declared canonical: what your page says through its canonical tag or header.
  • Google-selected canonical: what Google decided to index.

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.

Step 2: Protocol, host and trailing slash duplicates

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.

Step 3: URL parameters

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.

  • Self-referencing canonicals handle most of it. WordPress core prints a canonical on single posts and pages that ignores query strings, and SEO plugins extend this to archives. Confirm with the curl check above on a URL with ?utm_source=test added: the canonical should be the clean URL.
  • Do not link internally with parameters. Internal links with ?utm_ tags send a conflicting signal and break your own analytics attribution.
  • Shop filters and sorting. WooCommerce sorting (?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. Yoast SEO and other plugins include an option to remove ?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.

Step 4: Get an indexed staging site out of Google

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:

  1. Verify the staging host in Search Console as its own property, then open Indexing, Removals, New request and choose Remove all URLs with this prefix using 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.
  2. Add an X-Robots-Tag: noindex header on the staging host. Anything Google can still reach carries a noindex.
  3. Put the staging site behind a password (HTTP basic auth or your host’s staging protection). Google then gets a 401 response, cannot see the content, and drops the URLs over time. The setup for Apache and Nginx is in FG-80.

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.

Step 5: Attachment, print, archive and paginated pages

Attachment pages

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 and alternate versions

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.

Tag, category and author archives

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.

Pagination

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.

Step 6: Use rel=canonical correctly

A canonical is a hint, so it only works when the rest of your site agrees with it. The rules that matter:

  • Use the full absolute URL, including https:// and the host.
  • Output exactly one canonical per page, inside the <head>. A canonical in the body, or two conflicting ones, may be ignored.
  • Point only at a page that returns 200, is indexable and has substantially the same content. Canonicals to redirects, 404s or noindexed pages send mixed signals.
  • Keep sitemaps, internal links and hreflang consistent with the canonical. Do not list duplicate URLs in your sitemap.
  • Do not use noindex or robots.txt as a way to pick a canonical.

HTTP header canonicals for PDFs and other files

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.

Step 7: Check your SEO plugin settings

  • Only one plugin should output canonicals. Running two SEO plugins, or a theme with built-in SEO features plus a plugin, often produces two canonical tags.
  • Per-page canonical fields. Yoast SEO and Rank Math have a canonical URL field in each post’s advanced settings. Leave it empty to use the automatic self-referencing canonical. A value copied from staging or from another page is a common hidden mistake.
  • Archive and media settings. Review settings for tag, author and date archives, and for attachment redirects, as described in Step 5.

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.

Advanced: audit canonicals across the whole sitemap

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.

When to contact your host

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:

  • The URLs involved and the full curl -sI output for each, with any CDN ray or request IDs.
  • A screenshot of URL Inspection showing User-declared and Google-selected canonicals.
  • The staging or temporary hostname and the result of a site: search for it.
  • The date and time in UTC when you noticed the problem, and any recent migration or caching changes.

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.

Still stuck?

Print a free one-page checklist to keep, or open the Emergency Room if the symptom does not match.

Get the free checklists

Keep reading

All field guides

Search the field manual

What is going wrong?

Press / to search from any page, Esc to close