Fixing Duplicate Canonical Tags in SPAs

A SPA that injects a canonical tag on every route change β€” without removing the previous one β€” accumulates multiple <link rel="canonical"> elements in the head. Crawlers see conflicting signals, treat them as ambiguous, and may pick their own canonical or split ranking signals across URLs. This is a pure lifecycle bug, and the fix is to mutate one tag instead of appending many. It is a common failure within canonical URL management.

Step-by-step fix

  1. Detect the duplicates. In the console on a navigated route, count canonical tags.

    document.querySelectorAll('link[rel="canonical"]').length // > 1 means duplicates
  2. Stop appending; mutate the existing tag. The root cause is creating a new element on each navigation.

    // ❌ Appends a new canonical on every route change
    const link = document.createElement('link');
    link.rel = 'canonical'; link.href = url;
    document.head.appendChild(link);
    
    // βœ… Reuse one tag; update its href in place
    function setCanonical(url) {
      let link = document.querySelector('link[rel="canonical"]');
      if (!link) {
        link = document.createElement('link');
        link.rel = 'canonical';
        document.head.appendChild(link);
      }
      link.href = url; // single source of truth, mutated each route
    }
  3. Clean up any pre-existing duplicates on init so a stale build does not leave extras behind.

    const links = document.querySelectorAll('link[rel="canonical"]');
    links.forEach((l, i) => { if (i > 0) l.remove(); }); // keep one
  4. Hook it into the router so setCanonical runs on every navigation with the route’s clean URL.

Validation

  • document.querySelectorAll('link[rel="canonical"]').length equals 1 on every route.
  • GSC URL Inspection reports a single user-declared canonical.
  • Rendered HTML (View Tested Page) contains exactly one canonical tag.
  • Navigating between routes does not increase the canonical count.

Reference

// Router-integrated canonical manager β€” exactly one tag, mutated per route
export function installCanonical(router) {
  const link = document.querySelector('link[rel="canonical"]')
    || Object.assign(document.head.appendChild(document.createElement('link')), { rel: 'canonical' });
  // remove any extras left by the initial HTML
  document.querySelectorAll('link[rel="canonical"]').forEach((l) => { if (l !== link) l.remove(); });
  router.afterEach((to) => { link.href = new URL(to.path, location.origin).href; });
}

Frequently Asked Questions

What happens if a page has two canonical tags? Google treats conflicting canonical tags as an ambiguous signal and may ignore both, choosing its own canonical instead. That can lead to the wrong URL being indexed or ranking signals being split, which is why exactly one canonical per page is required.

Why does my SPA keep adding canonical tags? Each route change appends a new canonical link without removing the previous one. The fix is to mutate a single existing tag’s href on navigation rather than creating a new element every time.

← Back to Canonical URL Management in SPAs