From 52e4060d76aee227b5dec60386bc55c0d8dbf636 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 13 Aug 2026 19:34:37 +0200 Subject: [PATCH] fix(server): prefer portable project icons Project icon discovery could select an ICO that native clients cannot decode even when a renderable SVG or PNG was available. Keep ICO as the final fallback and recognize common sized icon names. --- .../server/src/utils/project-icon.test.ts | 25 +++++++++++++++++-- packages/server/src/utils/project-icon.ts | 16 ++++++++---- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/server/src/utils/project-icon.test.ts b/packages/server/src/utils/project-icon.test.ts index 18289d848..e424fd1de 100644 --- a/packages/server/src/utils/project-icon.test.ts +++ b/packages/server/src/utils/project-icon.test.ts @@ -125,11 +125,11 @@ describe("findProjectIcon", () => { }); it("prioritizes favicon over logo", async () => { - writeFileSync(join(tempDir, "favicon.ico"), "favicon"); + writeFileSync(join(tempDir, "favicon.png"), "favicon"); writeFileSync(join(tempDir, "logo.png"), "logo"); const result = await findProjectIcon(tempDir); - expect(result).toBe(join(tempDir, "favicon.ico")); + expect(result).toBe(join(tempDir, "favicon.png")); }); it("prioritizes priority dirs over root", async () => { @@ -180,6 +180,14 @@ describe("findProjectIcon", () => { expect(result).toBe(join(tempDir, "apple-touch-icon.png")); }); + it("prefers an Apple touch PNG over ICO", async () => { + writeFileSync(join(tempDir, "favicon.ico"), "ico"); + writeFileSync(join(tempDir, "apple-touch-icon.png"), "apple icon"); + + const result = await findProjectIcon(tempDir); + expect(result).toBe(join(tempDir, "apple-touch-icon.png")); + }); + it("finds icon-*.png patterns", async () => { writeFileSync(join(tempDir, "icon-192.png"), "192 icon"); @@ -187,6 +195,19 @@ describe("findProjectIcon", () => { expect(result).toBe(join(tempDir, "icon-192.png")); }); + it.each([ + "favicon-32x32.png", + "apple-touch-icon-180x180.png", + "android-chrome-192x192.png", + "safari-pinned-tab.svg", + "mstile-150x150.png", + ])("finds common sized icon %s", async (fileName) => { + writeFileSync(join(tempDir, fileName), "icon"); + + const result = await findProjectIcon(tempDir); + expect(result).toBe(join(tempDir, fileName)); + }); + it("handles non-existent directory gracefully", async () => { const result = await findProjectIcon(join(tempDir, "nonexistent")); expect(result).toBeNull(); diff --git a/packages/server/src/utils/project-icon.ts b/packages/server/src/utils/project-icon.ts index 259e29377..ed122b453 100644 --- a/packages/server/src/utils/project-icon.ts +++ b/packages/server/src/utils/project-icon.ts @@ -5,25 +5,31 @@ import { extname, join } from "path"; * Icon file patterns to search for, in priority order. * Patterns starting with '*' are glob patterns (e.g., icon-*.png). * - * Within each name, SVG and PNG outrank ICO: every client renders them (React - * Native's cannot decode ICO at all) and they scale better. ICO is the - * last resort for each name, not the first. + * SVG and PNG outrank ICO across all known names: every client renders them + * and they scale better. ICO is the final fallback because native clients + * cannot decode containers whose frames use the legacy bitmap format. */ export const ICON_PATTERNS = [ "favicon.svg", "favicon.png", - "favicon.ico", + "favicon-*.svg", + "favicon-*.png", "favico.svg", "favico.png", - "favico.ico", "icon.svg", "icon.png", "app-icon.svg", "app-icon.png", "apple-touch-icon.png", + "apple-touch-icon-*.png", "icon-*.png", + "android-chrome-*.png", + "safari-pinned-tab.svg", + "mstile-*.png", "logo.svg", "logo.png", + "favicon.ico", + "favico.ico", ]; /**