mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(favicon): harden sfw board icon swapping
This commit is contained in:
+2
-1
@@ -3,7 +3,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="referrer" content="no-referrer" />
|
<meta name="referrer" content="no-referrer" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon.ico?variant=nsfw" />
|
||||||
|
<link rel="shortcut icon" type="image/png" sizes="16x16" href="/favicon.ico?variant=nsfw" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name="description" content="5chan is a free, open-source, decentralized imageboard built on the Bitsocial protocol. No servers, no admins — all content is shared peer-to-peer via IPFS." />
|
<meta name="description" content="5chan is a free, open-source, decentralized imageboard built on the Bitsocial protocol. No servers, no admins — all content is shared peer-to-peer via IPFS." />
|
||||||
|
|||||||
@@ -7,19 +7,23 @@ describe('update-favicon', () => {
|
|||||||
vi.resetModules();
|
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');
|
const { updateFavicon } = await import('../update-favicon');
|
||||||
|
|
||||||
updateFavicon(false);
|
document.head.innerHTML = '<link rel="icon" href="/favicon.ico"><link rel="shortcut icon" href="/favicon.ico">';
|
||||||
expect(document.querySelectorAll('link[rel="icon"]')).toHaveLength(1);
|
|
||||||
expect(document.querySelector('link[rel="icon"]')?.getAttribute('href')).toBe('/favicon.ico');
|
|
||||||
|
|
||||||
updateFavicon(false);
|
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);
|
updateFavicon(true);
|
||||||
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('/favicon2.ico');
|
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 () => {
|
it('marks only non-special, non-routing aggregate sfw boards as sfw', async () => {
|
||||||
|
|||||||
+21
-11
@@ -1,24 +1,34 @@
|
|||||||
const DEFAULT_FAVICON = '/favicon.ico';
|
const DEFAULT_FAVICON = '/favicon.ico?variant=nsfw';
|
||||||
const SFW_FAVICON = '/favicon2.ico';
|
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;
|
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.
|
* 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 => {
|
export const updateFavicon = (isSfw: boolean): void => {
|
||||||
const href = isSfw ? SFW_FAVICON : DEFAULT_FAVICON;
|
const href = isSfw ? SFW_FAVICON : DEFAULT_FAVICON;
|
||||||
if (href === currentHref) return;
|
if (href === currentHref && hasExpectedFaviconLinks(href)) return;
|
||||||
currentHref = href;
|
currentHref = href;
|
||||||
|
|
||||||
const existing = document.querySelector<HTMLLinkElement>('link[rel="icon"]');
|
document.querySelectorAll<HTMLLinkElement>(FAVICON_SELECTOR).forEach((link) => link.remove());
|
||||||
if (existing) existing.remove();
|
FAVICON_RELS.forEach((rel) => {
|
||||||
|
document.head.appendChild(createFaviconLink(rel, href));
|
||||||
const link = document.createElement('link');
|
});
|
||||||
link.rel = 'icon';
|
|
||||||
link.href = href;
|
|
||||||
document.head.appendChild(link);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user