From 7dc31bd2cc7bfd12d976cdfdec5181fccd80bdbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 3 Sep 2026 00:21:05 -0400 Subject: [PATCH] fix(cli): score apple-touch-icon-precomposed at the 180px default (#3607) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit declaredSize() matched the rel token "apple-touch-icon" exactly, so the legacy "apple-touch-icon-precomposed" spelling (one token, not two) fell through to 0 instead of the 180px Apple default, even though the selector already collects it. Same page, one spelling apart, opposite winner inside tier 1 — it never drops a candidate and never beats the .ico tier. Switch to a startsWith check on the token. Exact-token matching stays for mask-icon, where a longer rel really would be a different asset. --- packages/cli/src/capture/faviconRanker.test.ts | 18 ++++++++++++++++++ packages/cli/src/capture/faviconRanker.ts | 10 +++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/capture/faviconRanker.test.ts b/packages/cli/src/capture/faviconRanker.test.ts index 669bc015f..ecfd01538 100644 --- a/packages/cli/src/capture/faviconRanker.test.ts +++ b/packages/cli/src/capture/faviconRanker.test.ts @@ -108,6 +108,24 @@ describe("rankIconCandidates", () => { expect(hrefs([{ rel: "icon", href: "" }, ...NOTION])).toHaveLength(2); }); + it("scores apple-touch-icon-precomposed at the same 180px default as apple-touch-icon", () => { + // Precomposed is one token longer, not a different rel: it must win against a small + // sized png the same way a plain apple-touch-icon would. + const precomposed: IconCandidate[] = [ + { + rel: "apple-touch-icon-precomposed", + href: "https://x.test/apple-touch-icon-precomposed.png", + sizes: null, + type: null, + }, + { rel: "icon", href: "https://x.test/favicon-32.png", sizes: "32x32", type: "image/png" }, + ]; + expect(hrefs(precomposed)).toEqual([ + "https://x.test/apple-touch-icon-precomposed.png", + "https://x.test/favicon-32.png", + ]); + }); + it("keeps DOM order between candidates of equal rank", () => { const same: IconCandidate[] = [ { rel: "icon", href: "https://x.test/a.png", sizes: "32x32" }, diff --git a/packages/cli/src/capture/faviconRanker.ts b/packages/cli/src/capture/faviconRanker.ts index 424189783..c97460909 100644 --- a/packages/cli/src/capture/faviconRanker.ts +++ b/packages/cli/src/capture/faviconRanker.ts @@ -68,7 +68,15 @@ function isIco(c: IconCandidate): boolean { function declaredSize(c: IconCandidate): number { const parsed = parseSizes(c.sizes); if (parsed > 0) return parsed; - return c.rel.toLowerCase().split(/\s+/).includes("apple-touch-icon") ? APPLE_TOUCH_DEFAULT_PX : 0; + // `startsWith`, not an exact-token match: `apple-touch-icon-precomposed` is the same + // 180px default, one spelling later. Exact match is right for `mask-icon` (isMaskIcon), + // where a longer rel really would be a different asset. + return c.rel + .toLowerCase() + .split(/\s+/) + .some((t) => t.startsWith("apple-touch-icon")) + ? APPLE_TOUCH_DEFAULT_PX + : 0; } function tierOf(c: IconCandidate): number {