Skip to content

fix: scope alternate link dedupeKey by href when type is used#759

Merged
harlan-zw merged 2 commits into
unjs:mainfrom
EduardF1:fix/alternate-link-deduplication
May 8, 2026
Merged

fix: scope alternate link dedupeKey by href when type is used#759
harlan-zw merged 2 commits into
unjs:mainfrom
EduardF1:fix/alternate-link-deduplication

Conversation

@EduardF1

@EduardF1 EduardF1 commented May 4, 2026

Copy link
Copy Markdown
Contributor

Problem

Multiple RSS/Atom alternate tags with the same ype but different hrefs are incorrectly deduplicated into a single tag.

Root cause

In dedupe.ts, the deduplication key for
el="alternate" links is built as:

s const altKey = props.hreflang || props.type if (altKey) return alternate:${altKey}

When hreflang is absent (e.g. RSS/Atom feeds), all feeds with the same ype (e.g. �pplication/rss+xml) share the same key — �lternate:application/rss+xml — regardless of their href. This collapses all but one of them.

Fix

Use hreflang alone as the key (language variants are uniquely identified by it). For type-based alternates (RSS/Atom), include href in the key so distinct feeds are not conflated:

s if (t === 'link' && props.rel === 'alternate') { if (props.hreflang) return alternate:${props.hreflang}if (props.type) returnalternate:${props.type}:${props.href || ''}}

Fixes #758

Summary by CodeRabbit

  • Bug Fixes
    • Improved deduplication for alternate link tags: hreflang values are treated separately, and type-based deduplication now also considers href to avoid collapsing distinct alternates.
    • Updated tests to cover RSS/feed deduplication: identical type+href dedupes, different hrefs are preserved, and behavior is consistent across consecutive updates.

@coderabbitai

coderabbitai Bot commented May 4, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

dedupeKey() for <link rel="alternate"> was changed to generate separate dedupe keys: when hreflang exists it uses that, otherwise it uses type plus href. Unit tests were updated to assert correct deduplication for RSS feeds with same/different hrefs and across separate pushes.

Changes

Link Deduplication Key & Tests

Layer / File(s) Summary
Core Logic
packages/unhead/src/utils/dedupe.ts
dedupeKey() branch for rel="alternate" now returns alternate:${props.hreflang} if hreflang exists; otherwise `alternate:${props.type}:${props.href
Behavioral Tests
packages/unhead/test/unit/server/deduping.test.ts
Replaced the old single-type dedupe test with three tests covering: dedupe on rehydration for same type+href, keeping multiple feeds with same type but different hrefs, and preserving multiple feeds across separate head.push calls.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibble keys with careful paws,
hreflang hops left, href trots because.
RSS flowers, each one a bloom,
No more crowding in the same room.
Happy feeds—no more gloom!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description provides problem statement, root cause analysis, and the fix with code example. However, it doesn't follow the repository template with sections like 'Type of change' checkboxes.
Linked Issues check ✅ Passed The PR successfully addresses issue #758 by modifying dedupeKey to include href in the key when type is present but hreflang is absent, and adds comprehensive tests covering both same-type same-href deduplication and multiple distinct feeds.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing the alternate link deduplication issue: the dedupe utility fix and corresponding tests directly address #758 without unrelated modifications.
Title check ✅ Passed The title clearly and concisely summarizes the main change: scoping the dedupe key for alternate links by href when type is used, which directly addresses the core fix described in the PR objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/unhead/src/utils/dedupe.ts (1)

16-21: ⚡ Quick win

Consider adding a regression test for the two-feed scenario.

No test file is visible in this PR. Without a test covering rel="alternate" links with the same type but different href values, this fix could silently regress. A minimal test asserting both keys are distinct (and that dedupeKey returns different values for the two links) would lock in the behaviour.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/unhead/src/utils/dedupe.ts` around lines 16 - 21, Add a regression
test that covers the two-feed scenario for rel="alternate" links: create two
link entries with t === 'link', rel === 'alternate', the same props.type but
different props.href (and one with props.hreflang absent), call the dedupe key
function (dedupeKey) or run the dedupe pipeline that uses it, and assert that
dedupeKey returns different strings for each link and that both links are
preserved after deduplication; this ensures the branch in dedupe.ts that returns
`alternate:${props.type}:${props.href || ''}` remains correct and prevents
future regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/unhead/src/utils/dedupe.ts`:
- Around line 16-21: Add a regression test that covers the two-feed scenario for
rel="alternate" links: create two link entries with t === 'link', rel ===
'alternate', the same props.type but different props.href (and one with
props.hreflang absent), call the dedupe key function (dedupeKey) or run the
dedupe pipeline that uses it, and assert that dedupeKey returns different
strings for each link and that both links are preserved after deduplication;
this ensures the branch in dedupe.ts that returns
`alternate:${props.type}:${props.href || ''}` remains correct and prevents
future regressions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8f0558ab-3d3d-4e34-8909-626a96969808

📥 Commits

Reviewing files that changed from the base of the PR and between e8e95b3 and c358806.

📒 Files selected for processing (1)
  • packages/unhead/src/utils/dedupe.ts

Reframes the existing 'dedupes RSS feeds with same type' test (which
asserted the buggy collapse-on-type behaviour) to cover the correct
rehydration case (same type + same href dedupes), and adds two new
regression tests for issue unjs#758 — multiple RSS feeds with the same
type but different hrefs must coexist, both within a single push and
across separate head.push calls.
@EduardF1

EduardF1 commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

Added regression coverage in packages/unhead/test/unit/server/deduping.test.ts (commit 71198cc):

  • dedupes RSS feeds with same type and same href on rehydration — same input pushed twice still collapses to one tag (the SSR + hydration case).
  • keeps multiple RSS feeds with same type and different hrefs (#758) — the literal scenario from the issue, in a single head.push.
  • keeps multiple RSS feeds across separate head pushes (#758) — same scenario but split across two pushes (mirrors how Vue/Nuxt composables register tags from different components).

I also rewrote the existing dedupes RSS feeds with same type test, which had been asserting the buggy collapse-on-type behaviour (it expected feed2.xml to overwrite feed.xml when the only differing attribute was href, and asserted the result not to contain feed.xml). With the dedupe key now scoped by href, that expectation no longer matches the intended behaviour from #758, so the rewritten version covers the rehydration semantics it was probably meant to assert in the first place.

Full pnpm vitest run packages/unhead is green locally (802 passed, 8 skipped).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/unhead/test/unit/server/deduping.test.ts (1)

584-600: 💤 Low value

New test duplicates existing coverage at lines 526–553.

dedupes RSS feeds with same type and same href on rehydration is functionally identical to the pre-existing dedupes alternate links without hreflang using href test (lines 526–553): both push the same application/rss+xml + feed.xml link twice and assert exactly one tag survives. The existing test is stricter (uses toMatchInlineSnapshot). The new test also passes under the old broken logic (type-only key still deduplicates same-type+same-href entries), so it provides no regression guard for the actual fix.

Consider removing this block and relying on the existing test at lines 526–553 for that scenario.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/unhead/test/unit/server/deduping.test.ts` around lines 584 - 600,
Remove the redundant test named "dedupes RSS feeds with same type and same href
on rehydration" (the it(...) block that pushes two identical link entries with
type 'application/rss+xml' and href 'https://example.com/feed.xml'), since it
duplicates the existing "dedupes alternate links without hreflang using href"
coverage; delete the entire it(...) block to avoid duplicate assertions and rely
on the stricter existing test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/unhead/test/unit/server/deduping.test.ts`:
- Around line 584-600: Remove the redundant test named "dedupes RSS feeds with
same type and same href on rehydration" (the it(...) block that pushes two
identical link entries with type 'application/rss+xml' and href
'https://example.com/feed.xml'), since it duplicates the existing "dedupes
alternate links without hreflang using href" coverage; delete the entire it(...)
block to avoid duplicate assertions and rely on the stricter existing test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 32ba04a0-0a75-4da3-80b0-302ffb7e2c5e

📥 Commits

Reviewing files that changed from the base of the PR and between c358806 and 71198cc.

📒 Files selected for processing (1)
  • packages/unhead/test/unit/server/deduping.test.ts

@EduardF1

EduardF1 commented May 7, 2026

Copy link
Copy Markdown
Contributor Author

Heads-up on the red analyze-bundle-size check: the bundle-size analysis itself ran fine (numbers below). The job fails on the final marocchino/sticky-pull-request-comment step with Resource not accessible by integration, which is the default GITHUB_TOKEN permission limitation when the action runs from a fork PR. Not an actionable PR-side failure.

Bundle Size Gzipped
Client (Minimal) 10.8 kB 4.4 kB
Server (Minimal) 10.7 kB 4.3 kB
Vue Client (Minimal) 11.8 kB 4.9 kB
Vue Server (Minimal) 11.6 kB 4.7 kB

@harlan-zw harlan-zw changed the title fix(dedupe): scope alternate link dedupeKey by href when type is used fix: scope alternate link dedupeKey by href when type is used May 8, 2026
@harlan-zw harlan-zw merged commit e869944 into unjs:main May 8, 2026
4 of 5 checks passed
@harlan-zw

Copy link
Copy Markdown
Collaborator

Thanks!

harlan-zw added a commit that referenced this pull request May 8, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Aggressive deduplication prevents multiple RSS feeds (rel="alternate") - custom key is ignored

2 participants