Skip to content

Commit 10846b5

Browse files
authored
fix: scope alternate link dedupeKey by href when type is used (#760)
Backport of #759 to v2. Multiple RSS/Atom alternate links sharing the same `type` but with different `href` values were being collapsed into one tag because the dedupe key was built from `type` alone. Include `href` in the key for type-based alternates so distinct feeds are preserved. `hreflang` precedence is unchanged. Fixes #758
1 parent 6a16082 commit 10846b5

2 files changed

Lines changed: 38 additions & 7 deletions

File tree

‎packages/unhead/src/utils/dedupe.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ export function dedupeKey<T extends HeadTag>(tag: T): string | undefined {
3131

3232
// dedupe alternate links with hreflang/type by that attribute
3333
if (name === 'link' && props.rel === 'alternate') {
34-
const altKey = props.hreflang || props.type
35-
if (altKey) {
36-
return `alternate:${altKey}`
37-
}
34+
if (props.hreflang)
35+
return `alternate:${props.hreflang}`
36+
if (props.type)
37+
return `alternate:${props.type}:${props.href || ''}`
3838
}
3939

4040
if (props.charset)

‎packages/unhead/test/unit/server/deduping.test.ts‎

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,7 @@ describe('dedupe', () => {
644644
expect(headTags).toContain('hreflang="fr"')
645645
})
646646

647-
it('dedupes RSS feeds with same type', async () => {
647+
it('dedupes RSS feeds with same type and same href on rehydration', async () => {
648648
const head = createServerHeadWithContext()
649649
head.push({
650650
link: [
@@ -653,13 +653,44 @@ describe('dedupe', () => {
653653
})
654654
head.push({
655655
link: [
656-
{ rel: 'alternate', type: 'application/rss+xml', href: 'https://example.com/feed2.xml', title: 'RSS Feed 2' },
656+
{ rel: 'alternate', type: 'application/rss+xml', href: 'https://example.com/feed.xml', title: 'RSS Feed' },
657657
],
658658
})
659659
const { headTags } = await renderSSRHead(head)
660660
expect(headTags.split('rel="alternate"').length).toBe(2)
661+
expect(headTags).toContain('feed.xml')
662+
})
663+
664+
it('keeps multiple RSS feeds with same type and different hrefs (#758)', async () => {
665+
const head = createServerHeadWithContext()
666+
head.push({
667+
link: [
668+
{ rel: 'alternate', type: 'application/rss+xml', href: 'https://example.com/feed.xml', title: 'RSS Feed' },
669+
{ rel: 'alternate', type: 'application/rss+xml', href: 'https://example.com/feed2.xml', title: 'RSS Feed 2' },
670+
],
671+
})
672+
const { headTags } = await renderSSRHead(head)
673+
expect(headTags.split('rel="alternate"').length).toBe(3)
674+
expect(headTags).toContain('feed.xml')
675+
expect(headTags).toContain('feed2.xml')
676+
})
677+
678+
it('keeps multiple RSS feeds across separate head pushes (#758)', async () => {
679+
const head = createServerHeadWithContext()
680+
head.push({
681+
link: [
682+
{ rel: 'alternate', type: 'application/rss+xml', href: 'https://example.com/feed.xml', title: 'RSS Feed' },
683+
],
684+
})
685+
head.push({
686+
link: [
687+
{ rel: 'alternate', type: 'application/rss+xml', href: 'https://example.com/feed2.xml', title: 'RSS Feed 2' },
688+
],
689+
})
690+
const { headTags } = await renderSSRHead(head)
691+
expect(headTags.split('rel="alternate"').length).toBe(3)
692+
expect(headTags).toContain('feed.xml')
661693
expect(headTags).toContain('feed2.xml')
662-
expect(headTags).not.toContain('feed.xml"')
663694
})
664695

665696
it('allows RSS and Atom feeds to coexist', async () => {

0 commit comments

Comments
 (0)