Skip to content

head: child route links cannot override parent route links (e.g. rel="canonical") #6719

@aladdin-im

Description

@aladdin-im

Which project does this relate to?

Start

Describe the bug

Describe the bug

When defining links in the head option of both a parent route and a child route, the links from all matched routes are simply concatenated without any deduplication logic. This means a child route cannot override a parent route's link — for example, <link rel="canonical"> defined in the root route cannot be overridden by a child route's canonical link. Both end up in the rendered HTML, which is invalid and harmful for SEO.

This is inconsistent with how meta tags are handled. Meta tags are correctly deduplicated by name or property attribute, with child routes taking priority over parent routes. Links have no such mechanism.

Your Example Website or App

Minimal reproduction:

// __root.tsx
export const Route = createRootRoute({
  head: () => ({
    links: [
      { rel: 'canonical', href: 'https://example.com/' },
    ],
  }),
  // ...
})

// blog/index.tsx
export const Route = createFileRoute('/blog/')({
  head: () => ({
    links: [
      { rel: 'canonical', href: 'https://example.com/blog' },
    ],
  }),
  // ...
})

When visiting /blog, the rendered HTML contains both canonical links:

<link rel="canonical" href="https://example.com/" />
<link rel="canonical" href="https://example.com/blog" />

Expected: only https://example.com/blog should be present.

Steps to Reproduce

  1. Define a <link rel="canonical"> in the root route's head.links
  2. Define a different <link rel="canonical"> in a child route's head.links
  3. Navigate to the child route
  4. Inspect the rendered <head> — both canonical links are present

Expected behavior

Child route's links with the same rel attribute (at least for unique-by-nature rels like canonical) should override the parent route's, similar to how meta tags are deduplicated by name/property.

Source Code Analysis

In headContentUtils.js:

  • Meta tags (lines 17–49): iterated from last match (deepest child) to first (root), deduplicated by name/property — child wins.
  • Links (lines 75–83): simply state.matches.map(match => match.links).flat(1) — no dedup by rel or any other attribute.

The final uniqBy (line 140) deduplicates by JSON.stringify, but since two canonical links have different href values, they are not considered duplicates.

How often does this bug happen?

Every time

Platform

  • @tanstack/react-router: v1.132.0
  • OS: macOS
  • Browser: All

Your Example Website or App

/

Steps to Reproduce the Bug or Issue

/

Expected behavior

/

Screenshots or Videos

No response

Platform

  • @tanstack/react-router: v1.161.3
  • OS: macOS
  • Browser: All

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions