Skip to content

Commit b55ac74

Browse files
committed
fix: Deduplicate head link tags, including canonical, by processing matches from deepest to shallowest.
1 parent 2f41a1f commit b55ac74

3 files changed

Lines changed: 91 additions & 33 deletions

File tree

packages/react-router/src/headContentUtils.tsx

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,36 @@ export const useTags = () => {
9090

9191
const links = useRouterState({
9292
select: (state) => {
93-
const constructed = state.matches
94-
.map((match) => match.links!)
95-
.filter(Boolean)
96-
.flat(1)
97-
.map((link) => ({
98-
tag: 'link',
99-
attrs: {
100-
...link,
101-
nonce,
102-
},
103-
})) satisfies Array<RouterManagedTag>
93+
const constructedLinks: Array<RouterManagedTag> = []
94+
const relsToDedupe = new Set(['canonical'])
95+
const linksByRel: Record<string, true> = {}
96+
97+
for (let i = state.matches.length - 1; i >= 0; i--) {
98+
const match = state.matches[i]!
99+
const matchLinks = match.links
100+
if (!matchLinks) continue
101+
102+
for (let j = matchLinks.length - 1; j >= 0; j--) {
103+
const link = matchLinks[j]!
104+
if (link.rel && relsToDedupe.has(link.rel)) {
105+
if (linksByRel[link.rel]) {
106+
continue
107+
}
108+
linksByRel[link.rel] = true
109+
}
110+
111+
constructedLinks.push({
112+
tag: 'link',
113+
attrs: {
114+
...link,
115+
nonce,
116+
},
117+
})
118+
}
119+
}
120+
121+
constructedLinks.reverse()
122+
const constructed = constructedLinks satisfies Array<RouterManagedTag>
104123

105124
const manifest = router.ssr?.manifest
106125

packages/solid-router/src/headContentUtils.tsx

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -91,17 +91,36 @@ export const useTags = () => {
9191

9292
const links = useRouterState({
9393
select: (state) => {
94-
const constructed = state.matches
95-
.map((match) => match.links!)
96-
.filter(Boolean)
97-
.flat(1)
98-
.map((link) => ({
99-
tag: 'link',
100-
attrs: {
101-
...link,
102-
nonce,
103-
},
104-
})) satisfies Array<RouterManagedTag>
94+
const constructedLinks: Array<RouterManagedTag> = []
95+
const relsToDedupe = new Set(['canonical'])
96+
const linksByRel: Record<string, true> = {}
97+
98+
for (let i = state.matches.length - 1; i >= 0; i--) {
99+
const match = state.matches[i]!
100+
const matchLinks = match.links
101+
if (!matchLinks) continue
102+
103+
for (let j = matchLinks.length - 1; j >= 0; j--) {
104+
const link = matchLinks[j]!
105+
if (link.rel && relsToDedupe.has(link.rel)) {
106+
if (linksByRel[link.rel]) {
107+
continue
108+
}
109+
linksByRel[link.rel] = true
110+
}
111+
112+
constructedLinks.push({
113+
tag: 'link',
114+
attrs: {
115+
...link,
116+
nonce,
117+
},
118+
})
119+
}
120+
}
121+
122+
constructedLinks.reverse()
123+
const constructed = constructedLinks satisfies Array<RouterManagedTag>
105124

106125
const manifest = router.ssr?.manifest
107126

packages/vue-router/src/headContentUtils.tsx

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,37 @@ export const useTags = () => {
7474
})
7575

7676
const links = useRouterState({
77-
select: (state) =>
78-
state.matches
79-
.map((match) => match.links!)
80-
.filter(Boolean)
81-
.flat(1)
82-
.map((link) => ({
83-
tag: 'link',
84-
attrs: {
85-
...link,
86-
},
87-
})) as Array<RouterManagedTag>,
77+
select: (state) => {
78+
const constructedLinks: Array<RouterManagedTag> = []
79+
const relsToDedupe = new Set(['canonical'])
80+
const linksByRel: Record<string, true> = {}
81+
82+
for (let i = state.matches.length - 1; i >= 0; i--) {
83+
const match = state.matches[i]!
84+
const matchLinks = match.links
85+
if (!matchLinks) continue
86+
87+
for (let j = matchLinks.length - 1; j >= 0; j--) {
88+
const link = matchLinks[j]!
89+
if (link.rel && relsToDedupe.has(link.rel)) {
90+
if (linksByRel[link.rel]) {
91+
continue
92+
}
93+
linksByRel[link.rel] = true
94+
}
95+
96+
constructedLinks.push({
97+
tag: 'link',
98+
attrs: {
99+
...link,
100+
},
101+
})
102+
}
103+
}
104+
105+
constructedLinks.reverse()
106+
return constructedLinks satisfies Array<RouterManagedTag>
107+
},
88108
})
89109

90110
const preloadMeta = useRouterState({

0 commit comments

Comments
 (0)