From 73ad497fd1029a09ef98751bcc8abe65e28a4151 Mon Sep 17 00:00:00 2001 From: Leo Kettmeir Date: Wed, 27 May 2026 14:54:51 +0200 Subject: [PATCH] fix: stylesheet links in drop entry CSS in prod The Head cache key for excluded href for every rel, so any in collided with entry-asset stylesheets (e.g. Tailwind) and replaced them at the layout's position. The href exclusion was added to let override a singleton like canonical without a key; scope it to those rels so distinct stylesheets no longer dedupe. Closes #3824 --- .../fresh/src/runtime/server/preact_hooks.ts | 13 ++++++- packages/fresh/tests/head_test.tsx | 35 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/fresh/src/runtime/server/preact_hooks.ts b/packages/fresh/src/runtime/server/preact_hooks.ts index 8070db536a6..daca9e491ef 100644 --- a/packages/fresh/src/runtime/server/preact_hooks.ts +++ b/packages/fresh/src/runtime/server/preact_hooks.ts @@ -203,6 +203,14 @@ options[OptionsType.ATTR] = (name, value) => { const PATCHED = new WeakSet(); +// Link rels whose presence is conceptually singleton: a `` override +// should replace the existing tag regardless of its href. Other rels +// (stylesheet, preload, alternate, ...) can legitimately appear multiple +// times with different hrefs, so href must remain part of the cache key. +function isSingletonLinkRel(rel: unknown): boolean { + return rel === "canonical" || rel === "manifest"; +} + function normalizeKey(key: unknown): string { const value = key ?? ""; const s = (typeof value !== "string") ? String(value) : value; @@ -406,7 +414,10 @@ options[OptionsType.DIFF] = (vnode) => { continue; } else if (originalType === "meta" && key === "content") { continue; - } else if (originalType === "link" && key === "href") { + } else if ( + originalType === "link" && key === "href" && + isSingletonLinkRel(props.rel) + ) { continue; } diff --git a/packages/fresh/tests/head_test.tsx b/packages/fresh/tests/head_test.tsx index c5b94e08715..cc0d07891d9 100644 --- a/packages/fresh/tests/head_test.tsx +++ b/packages/fresh/tests/head_test.tsx @@ -156,6 +156,41 @@ Deno.test("Head - ssr - merge keyed", async () => { expect(last?.textContent).toEqual("ok"); }); +Deno.test("Head - ssr - stylesheet links with different hrefs coexist", async () => { + const handler = new App() + .appWrapper(({ Component }) => { + return ( + + + + + + + + + + ); + }) + .get("/", (ctx) => { + return ctx.render( + + + , + ); + }).handler(); + + const server = new FakeServer(handler); + const res = await server.get("/"); + const doc = parseHtml(await res.text()); + + const hrefs = Array.from( + doc.querySelectorAll("link[rel='stylesheet']"), + ).map((el) => (el as HTMLLinkElement).getAttribute("href")); + + expect(hrefs).toContain("/entry.css"); + expect(hrefs).toContain("https://fonts.example.com/font.css"); +}); + Deno.test("Head - ssr - updates link", async () => { const handler = new App() .appWrapper(({ Component }) => {