From 8a38e7ca769cc57f32f1f5db437420caae6cef60 Mon Sep 17 00:00:00 2001 From: Paul Hansen Date: Tue, 9 Dec 2025 07:53:42 -0600 Subject: [PATCH] fix: Allow links with duplicate href if rel is different (#5071) Fixes #5070 --- packages/document/src/elements/link.rs | 14 ++++++++---- packages/playwright-tests/web.spec.js | 16 ++++++++++++++ packages/playwright-tests/web/src/main.rs | 26 +++++++++++++++++++++++ 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/packages/document/src/elements/link.rs b/packages/document/src/elements/link.rs index c0bf4a5ac..68eb464fa 100644 --- a/packages/document/src/elements/link.rs +++ b/packages/document/src/elements/link.rs @@ -20,7 +20,7 @@ pub struct LinkProps { pub disabled: Option, pub r#as: Option, pub sizes: Option, - /// Links are deduplicated by their href attribute + /// Links are deduplicated by their href and rel attributes pub href: Option, pub crossorigin: Option, pub referrerpolicy: Option, @@ -122,7 +122,7 @@ pub fn Link(props: LinkProps) -> Element { let document = document(); let mut insert_link = document.create_head_component(); if let Some(href) = &props.href { - if !should_insert_link(href) { + if !should_insert_link(href, props.rel.as_deref()) { insert_link = false; } } @@ -140,8 +140,14 @@ pub fn Link(props: LinkProps) -> Element { #[derive(Default, Clone)] struct LinkContext(DeduplicationContext); -fn should_insert_link(href: &str) -> bool { +fn should_insert_link(href: &str, rel: Option<&str>) -> bool { + // Include rel in the deduplication key so that the same href can be used + // with different rel values (e.g., rel="preload" and rel="stylesheet") + let key = match rel { + Some(rel) => format!("{href}|{rel}"), + None => href.to_string(), + }; get_or_insert_root_context::() .0 - .should_insert(href) + .should_insert(&key) } diff --git a/packages/playwright-tests/web.spec.js b/packages/playwright-tests/web.spec.js index f46f91a57..bca9f52d0 100644 --- a/packages/playwright-tests/web.spec.js +++ b/packages/playwright-tests/web.spec.js @@ -156,6 +156,22 @@ test("document elements", async ({ page }) => { await expect(main).toHaveCSS("font-family", "Roboto"); }); +test("link preload and stylesheet with same href are not deduplicated", async ({ page }) => { + await page.goto("http://localhost:9990"); + + // Both links should exist (different rel = not deduplicated) + await expect(page.locator("link#dedup-preload[rel='preload']")).toHaveCount(1); + await expect(page.locator("link#dedup-stylesheet[rel='stylesheet']")).toHaveCount(1); +}); + +test("links with same href and rel are deduplicated", async ({ page }) => { + await page.goto("http://localhost:9990"); + + // Only first link should exist (same rel = deduplicated) + await expect(page.locator("link#dedup-first")).toHaveCount(1); + await expect(page.locator("link#dedup-second")).toHaveCount(0); +}); + test("merge styles", async ({ page }) => { await page.goto("http://localhost:9990"); // wait until the div is mounted diff --git a/packages/playwright-tests/web/src/main.rs b/packages/playwright-tests/web/src/main.rs index 600c6f755..2efc6cee4 100644 --- a/packages/playwright-tests/web/src/main.rs +++ b/packages/playwright-tests/web/src/main.rs @@ -154,6 +154,32 @@ fn DocumentElements() -> Element { document::Stylesheet { id: "stylesheet-head", href: "https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic" } document::Script { id: "script-head", async: true, "console.log('hello world');" } document::Style { id: "style-head", "body {{ font-family: 'Roboto'; }}" } + + // Test that links with same href but different rel are NOT deduplicated + // See https://github.com/DioxusLabs/dioxus/issues/5070 + document::Link { + id: "dedup-preload", + rel: "preload", + href: "dedup-test.css", + r#as: "style", + } + document::Link { + id: "dedup-stylesheet", + rel: "stylesheet", + href: "dedup-test.css", + } + + // Test that links with same href AND same rel ARE deduplicated + document::Link { + id: "dedup-first", + rel: "stylesheet", + href: "dedup-same.css", + } + document::Link { + id: "dedup-second", + rel: "stylesheet", + href: "dedup-same.css", + } } }