From 0b844e60601528a1aecf48d016eca67e23a66c7d Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Tue, 24 Mar 2026 16:03:31 +0700 Subject: [PATCH] fix(favicon): harden sfw board icon swapping --- index.html | 3 ++- src/lib/__tests__/update-favicon.test.ts | 18 +++++++------ src/lib/update-favicon.ts | 32 ++++++++++++++++-------- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/index.html b/index.html index 2944fc8f..f1c3f89a 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,8 @@ - + + diff --git a/src/lib/__tests__/update-favicon.test.ts b/src/lib/__tests__/update-favicon.test.ts index c5048d2a..91e6d8ac 100644 --- a/src/lib/__tests__/update-favicon.test.ts +++ b/src/lib/__tests__/update-favicon.test.ts @@ -7,19 +7,23 @@ describe('update-favicon', () => { vi.resetModules(); }); - it('creates a favicon link and replaces it only when the target icon changes', async () => { + it('replaces managed icon links and keeps both icon rel variants in sync', async () => { const { updateFavicon } = await import('../update-favicon'); - updateFavicon(false); - expect(document.querySelectorAll('link[rel="icon"]')).toHaveLength(1); - expect(document.querySelector('link[rel="icon"]')?.getAttribute('href')).toBe('/favicon.ico'); + document.head.innerHTML = ''; updateFavicon(false); - expect(document.querySelectorAll('link[rel="icon"]')).toHaveLength(1); + expect(document.querySelectorAll('link[rel="icon"], link[rel="shortcut icon"]')).toHaveLength(2); + expect(document.querySelector('link[rel="icon"]')?.getAttribute('href')).toBe('/favicon.ico?variant=nsfw'); + expect(document.querySelector('link[rel="shortcut icon"]')?.getAttribute('href')).toBe('/favicon.ico?variant=nsfw'); + + updateFavicon(false); + expect(document.querySelectorAll('link[rel="icon"], link[rel="shortcut icon"]')).toHaveLength(2); updateFavicon(true); - expect(document.querySelectorAll('link[rel="icon"]')).toHaveLength(1); - expect(document.querySelector('link[rel="icon"]')?.getAttribute('href')).toBe('/favicon2.ico'); + expect(document.querySelectorAll('link[rel="icon"], link[rel="shortcut icon"]')).toHaveLength(2); + expect(document.querySelector('link[rel="icon"]')?.getAttribute('href')).toBe('/favicon2.ico?variant=sfw'); + expect(document.querySelector('link[rel="shortcut icon"]')?.getAttribute('href')).toBe('/favicon2.ico?variant=sfw'); }); it('marks only non-special, non-routing aggregate sfw boards as sfw', async () => { diff --git a/src/lib/update-favicon.ts b/src/lib/update-favicon.ts index 4a4caacb..9a2ad347 100644 --- a/src/lib/update-favicon.ts +++ b/src/lib/update-favicon.ts @@ -1,24 +1,34 @@ -const DEFAULT_FAVICON = '/favicon.ico'; -const SFW_FAVICON = '/favicon2.ico'; +const DEFAULT_FAVICON = '/favicon.ico?variant=nsfw'; +const SFW_FAVICON = '/favicon2.ico?variant=sfw'; +const FAVICON_RELS = ['icon', 'shortcut icon'] as const; +const FAVICON_SELECTOR = FAVICON_RELS.map((rel) => `link[rel="${rel}"]`).join(', '); let currentHref: string | null = null; +const hasExpectedFaviconLinks = (href: string): boolean => FAVICON_RELS.every((rel) => document.querySelector(`link[rel="${rel}"][href="${href}"]`)); + +const createFaviconLink = (rel: (typeof FAVICON_RELS)[number], href: string): HTMLLinkElement => { + const link = document.createElement('link'); + link.rel = rel; + link.type = 'image/png'; + link.sizes = '16x16'; + link.href = href; + return link; +}; + /** * Swap the tab favicon between the default (NSFW/home) and SFW variants. - * Uses remove-and-recreate to bypass aggressive browser favicon caching. + * Uses remove-and-recreate plus cache-busted URLs to bypass sticky favicon caching. */ export const updateFavicon = (isSfw: boolean): void => { const href = isSfw ? SFW_FAVICON : DEFAULT_FAVICON; - if (href === currentHref) return; + if (href === currentHref && hasExpectedFaviconLinks(href)) return; currentHref = href; - const existing = document.querySelector('link[rel="icon"]'); - if (existing) existing.remove(); - - const link = document.createElement('link'); - link.rel = 'icon'; - link.href = href; - document.head.appendChild(link); + document.querySelectorAll(FAVICON_SELECTOR).forEach((link) => link.remove()); + FAVICON_RELS.forEach((rel) => { + document.head.appendChild(createFaviconLink(rel, href)); + }); }; /**