Skip to Content

Duplicate title, description, canonical or H1 on one page

What Is This Issue

A single page declares the same head element more than once — two <title> tags, two <meta name="description"> tags, two <link rel="canonical"> tags, or more than one <h1>.

These elements are singular by definition. A page has one title, one description, one canonical URL and one primary heading. When two appear, the page is giving two different answers to the same question, and search engines pick one by rules that are neither documented nor guaranteed to stay the same between crawls.

This is a within-one-page check, matching the issue’s definition: “No duplicate elements, on page only”. It has nothing to do with two different URLs serving the same content — that is a separate concern, covered by the canonical checks (#6, #172, #174).

Where the duplicates usually come from:

  • An SEO plugin adds a title or description the theme already output
  • A layout template and a page template both render a canonical tag
  • A CMS injects a description alongside one hand-written in the template
  • A hero component and a content block each render an <h1>
  • A partial is included twice on a page that reuses it in two slots

Why Is This Important

Which duplicate wins is not something you control, and often not the one you would have chosen.

  • The search result may not say what you wrote. With two <title> tags, browsers use the first and Google is free to use either — or to substitute its own. The title is the single largest influence on whether someone clicks, and it becomes a coin toss.
  • Two canonical tags cancel each other out. Google’s documented behaviour is to ignore conflicting canonical signals entirely, so a page that appeared to specify a preferred URL is treated as if it never did. The consolidation you were relying on silently stops happening.
  • Two <h1> elements blur the page’s topic. The primary heading tells search engines and screen-reader users what the page is about. Two of them means neither is primary.
  • It hides drift. Two sources of truth diverge over time: a plugin is updated, a theme is not, and the page now claims two different descriptions with nobody aware.
  • It is usually a symptom. A duplicated element almost always means two systems are both managing your head tags without knowing about each other — a problem larger than this one page, and one that will keep producing new instances until it is resolved.

How XeoPix Detects This

  1. Fetches the page and parses the raw HTML, following redirects to the final response.

  2. Counts four singular elements in that one document:

    ElementSelector
    Titlehead > title
    Meta descriptionmeta[name="description"]
    Canonicallink[rel="canonical"]
    Primary headingh1
  3. Records any element found more than once, along with the value of the first occurrence and the total count — so the finding names not just what was duplicated but which value the page is currently leading with.

  4. Raises one finding per page listing every duplicated element, rather than one finding per element. A page with two titles and two canonicals is one thing to fix in one place.

The title selector is scoped to the document head deliberately. <title> also appears inside <svg>, where it is the accessible name for an icon rather than a document title; counting those reported “duplicate title elements” on any page carrying two or more inline SVG icons, which is most of them.

This check looks at one page in isolation. Whether two different URLs serve the same content is a separate question, answered by the canonical checks — #6 (a canonical is present), #172 (it matches the sitemap) and #174 (it is self-referencing).

Note: detection uses raw HTML only — no JavaScript is executed. An element added to the head client-side after load will not be counted here, so if a script injects meta tags, check the rendered DOM as well.

How To Fix

  1. See which element is duplicated. The issue detail lists each duplicated element, its first value, and how many were found — for example title, "Acme — Home", count 2.

  2. Find every copy in the rendered output. View the page source (not the template) and search for the element. The count in the finding tells you how many to expect. It is almost always the rendered page, rather than any single template, that reveals the second one.

  3. Identify which system emits each copy. In practice these are the usual pairs:

    • An SEO plugin and the theme’s <head> partial
    • A base layout and a page-specific layout that also opens a <head> block
    • A CMS field and a hard-coded fallback that was never removed
    • For <h1>: a page-title component and a hero or banner component
  4. Pick one owner and turn off the others. Most SEO plugins have per-tag switches for exactly this reason. Prefer disabling the duplicate over deleting the plugin’s output, so the tag keeps a single, deliberate source.

  5. For <h1>, demote rather than delete. A second <h1> is usually a section heading that reached for the wrong level. Change it to <h2> and keep the heading hierarchy intact — do not remove the text.

  6. Add a guard so it cannot come back. A template that renders <head> tags should render them once, from one place. If your framework supports a head-management layer (a <Head> component, a head block, a meta manager), route every tag through it so the last writer wins deterministically instead of both appearing.

  7. Re-scan and confirm the count is one.

What We Store

Storage Level

Page Level — This issue is evaluated for each individual URL.


Database Table / Prisma Model

PageSeoBasicsData


Stored Fields

FieldTypeDescription
hasDuplicateElementsBoolean?Whether the page has duplicate title/meta tags
duplicateElementsJson?Details of duplicate elements found

Detection Dependencies

  • The following data sources are required to evaluate this issue:
  • HTML Document — The crawler checks for multiple <title> tags, multiple <meta name="description"> tags, or multiple canonical tags

Examples

Two <title> tags

The theme renders one and an SEO plugin renders another. Browsers show the first; Google may use either, or replace both with something of its own.

<!-- Fails: two titles, two different answers --> <head> <title>Acme — Home</title> <meta name="description" content="Widgets for every workshop." /> <title>Acme Ltd | Industrial Widgets Since 1974</title> </head>
<!-- Passes: one owner for the tag --> <head> <title>Acme Ltd | Industrial Widgets Since 1974</title> <meta name="description" content="Widgets for every workshop." /> </head>

Two canonical tags

Google’s documented behaviour for conflicting canonicals is to ignore them entirely, so this page has no canonical at all despite appearing to specify one twice.

<!-- Fails: the two disagree, so neither is used --> <head> <link rel="canonical" href="https://example.com/product" /> <link rel="canonical" href="https://example.com/product?color=blue" /> </head>
<!-- Passes --> <head> <link rel="canonical" href="https://example.com/product" /> </head>

Two <h1> elements

Almost always a section heading that reached for the wrong level. Demote it rather than deleting the text.

<!-- Fails: the page has two primary topics --> <body> <header><h1>Acme Ltd</h1></header> <main><h1>Industrial Widgets</h1></main> </body>
<!-- Passes: one primary heading, the rest in hierarchy --> <body> <header><p class="brand">Acme Ltd</p></header> <main> <h1>Industrial Widgets</h1> <h2>Sizes and finishes</h2> </main> </body>

An inline SVG <title> is not a duplicate

<title> inside an <svg> is the accessible name for that graphic. It is not a document title and is not reported here.

<!-- Passes: one document title, plus accessible names for two icons --> <head> <title>Acme — Search</title> </head> <body> <svg aria-hidden="false"><title>Search</title><path d="…" /></svg> <svg aria-hidden="false"><title>Close</title><path d="…" /></svg> </body>

Two meta descriptions

<!-- Fails: a CMS field and a hard-coded fallback both rendered --> <head> <meta name="description" content="Widgets for every workshop." /> <meta name="description" content="Welcome to our website." /> </head>

Unit Test

Test File

__tests__/seo-audit-checks/pageSeoBasics/issue-43-canonical-points-https.test.js

Purpose

Validates that the SEO audit correctly detects canonical URLs that point to HTTP instead of HTTPS on HTTPS-served pages.

Tested Function

runPageSeoBasics() from toggleGroups/pageSeoBasics.js

Issue Information

  • Issue Number: 43
  • Issue Code: CANONICAL_POINTS_HTTPS (canonical_points_https)
  • Toggle Group: pageSeoBasics

Test Scenarios

Positive Test Cases

  1. Canonical uses HTTPS — Canonical URL uses https://. No issue should be reported.
  2. No canonical tag — Page has no <link rel="canonical"> tag. No issue should be reported.
  3. Protocol-relative URL — Canonical uses a protocol-relative URL (//example.com/page). No issue should be reported.

Negative Test Cases

  1. Canonical uses HTTP — Canonical URL uses http:// instead of https://. The issue should be reported with details.canonicalUrl containing the HTTP URL.
  2. HTTP with non-standard port — Canonical uses http:// with a non-standard port (:8080). The issue should be reported.

Boundary Cases

None.

Edge Cases

  1. Protocol-relative URL — Canonical URLs starting with // are not flagged as HTTP.
  2. HTTP with non-standard port — Even with a port number, HTTP canonical URLs are still detected.
  3. Empty HTML — The function handles empty HTML input without crashing.

Expected Outcome

Pass

The issue is not reported when the canonical URL uses HTTPS, is protocol-relative, or is absent.

Fail

The issue is reported when the canonical URL starts with http://.

Validation

  • Verifies correct issue detection for HTTP canonical URLs
  • Verifies no issue is reported for HTTPS canonical URLs
  • Verifies no issue is reported when no canonical tag exists
  • Verifies protocol-relative URLs are not flagged
  • Verifies the details.canonicalUrl contains the HTTP URL
  • Verifies HTTP with non-standard port is still detected
  • Verifies empty HTML does not cause a crash
  • toggleGroups/pageSeoBasics.js
  • issueCodes.js
  • utils/issues.js

Coverage Summary

  • Covers HTTPS canonical URL (pass)
  • Covers no canonical tag (pass)
  • Covers protocol-relative canonical URL (pass)
  • Covers HTTP canonical URL (fail)
  • Covers HTTP canonical URL with non-standard port (fail)
  • Covers empty HTML error handling (no crash)

References

Last updated on