Skip to content

Commit 56faed1

Browse files
authored
fix: stylesheet links in <Head> drop entry CSS (#3825)
The Head cache key for <link> excluded href for every rel, so any <link rel="stylesheet"> in <Head> collided with entry-asset stylesheets (e.g. Tailwind) and replaced them at the layout's <head> position. The href exclusion was added to let <Head> override a singleton like canonical without a key; scope it to those rels so distinct stylesheets no longer dedupe. Closes #3824
1 parent 3afaaf4 commit 56faed1

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

packages/fresh/src/runtime/server/preact_hooks.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,14 @@ options[OptionsType.ATTR] = (name, value) => {
203203

204204
const PATCHED = new WeakSet<VNode>();
205205

206+
// Link rels whose presence is conceptually singleton: a `<Head>` override
207+
// should replace the existing tag regardless of its href. Other rels
208+
// (stylesheet, preload, alternate, ...) can legitimately appear multiple
209+
// times with different hrefs, so href must remain part of the cache key.
210+
function isSingletonLinkRel(rel: unknown): boolean {
211+
return rel === "canonical" || rel === "manifest";
212+
}
213+
206214
function normalizeKey(key: unknown): string {
207215
const value = key ?? "";
208216
const s = (typeof value !== "string") ? String(value) : value;
@@ -406,7 +414,10 @@ options[OptionsType.DIFF] = (vnode) => {
406414
continue;
407415
} else if (originalType === "meta" && key === "content") {
408416
continue;
409-
} else if (originalType === "link" && key === "href") {
417+
} else if (
418+
originalType === "link" && key === "href" &&
419+
isSingletonLinkRel(props.rel)
420+
) {
410421
continue;
411422
}
412423

packages/fresh/tests/head_test.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,41 @@ Deno.test("Head - ssr - merge keyed", async () => {
156156
expect(last?.textContent).toEqual("ok");
157157
});
158158

159+
Deno.test("Head - ssr - stylesheet links with different hrefs coexist", async () => {
160+
const handler = new App()
161+
.appWrapper(({ Component }) => {
162+
return (
163+
<html lang="en">
164+
<head>
165+
<meta charset="utf-8" />
166+
<link rel="stylesheet" href="/entry.css" />
167+
</head>
168+
<body>
169+
<Component />
170+
</body>
171+
</html>
172+
);
173+
})
174+
.get("/", (ctx) => {
175+
return ctx.render(
176+
<Head>
177+
<link rel="stylesheet" href="https://fonts.example.com/font.css" />
178+
</Head>,
179+
);
180+
}).handler();
181+
182+
const server = new FakeServer(handler);
183+
const res = await server.get("/");
184+
const doc = parseHtml(await res.text());
185+
186+
const hrefs = Array.from(
187+
doc.querySelectorAll("link[rel='stylesheet']"),
188+
).map((el) => (el as HTMLLinkElement).getAttribute("href"));
189+
190+
expect(hrefs).toContain("/entry.css");
191+
expect(hrefs).toContain("https://fonts.example.com/font.css");
192+
});
193+
159194
Deno.test("Head - ssr - updates link", async () => {
160195
const handler = new App()
161196
.appWrapper(({ Component }) => {

0 commit comments

Comments
 (0)