mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(favicon): show dedicated icon on 404 pages
Add favicon-404.ico and switch tab favicon on not-found routes while keeping static app pages and larger crawler icons unchanged.
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { useAccountComment } from '@bitsocial/bitsocial-react-hooks';
|
||||
import { isAllView, isModView, isSubscriptionsView } from '../lib/utils/view-utils';
|
||||
import { isAllView, isModView, isNotFoundView, isSubscriptionsView } from '../lib/utils/view-utils';
|
||||
import useThemeStore from '../stores/use-theme-store';
|
||||
import { useDirectories } from './use-directories';
|
||||
import { useResolvedCommunityAddress } from './use-resolved-community-address';
|
||||
@@ -22,9 +22,8 @@ const updateThemeClass = (newTheme: string) => {
|
||||
|
||||
const useTheme = (): [string, (theme: string) => void] => {
|
||||
const location = useLocation();
|
||||
const params = useParams<{ boardIdentifier?: string }>();
|
||||
const pendingPostParams = useParams<{ accountCommentIndex?: string }>();
|
||||
const pendingPost = useAccountComment({ commentIndex: normalizeAccountCommentIndex(pendingPostParams?.accountCommentIndex) });
|
||||
const params = useParams<{ accountCommentIndex?: string; boardIdentifier?: string; commentCid?: string }>();
|
||||
const pendingPost = useAccountComment({ commentIndex: normalizeAccountCommentIndex(params?.accountCommentIndex) });
|
||||
const pendingPostCommunityAddress = getCommentCommunityAddress(pendingPost);
|
||||
|
||||
const { isEnabled, setIsEnabled } = useSpecialThemeStore();
|
||||
@@ -36,6 +35,7 @@ const useTheme = (): [string, (theme: string) => void] => {
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
||||
const isInModView = isModView(location.pathname);
|
||||
const isInNotFoundView = isNotFoundView(location.pathname, params);
|
||||
const routeIdentifier = params.boardIdentifier;
|
||||
const resolvedAddress = useResolvedCommunityAddress();
|
||||
const communityAddress = resolvedAddress || pendingPostCommunityAddress || routeIdentifier;
|
||||
@@ -92,8 +92,8 @@ const useTheme = (): [string, (theme: string) => void] => {
|
||||
}, [currentTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
updateFavicon(sfw);
|
||||
}, [sfw]);
|
||||
updateFavicon(isInNotFoundView ? 'not-found' : sfw);
|
||||
}, [isInNotFoundView, sfw]);
|
||||
|
||||
const setCommunityTheme = useCallback(
|
||||
async (newTheme: string) => {
|
||||
|
||||
@@ -31,6 +31,23 @@ describe('update-favicon', () => {
|
||||
expect(document.querySelector('link[rel="apple-touch-icon"]')?.getAttribute('href')).toBe('/apple-touch-icon.png');
|
||||
});
|
||||
|
||||
it('can switch to the not-found ico favicon without removing larger crawler icons', async () => {
|
||||
const { updateFavicon } = await import('../update-favicon');
|
||||
|
||||
document.head.innerHTML =
|
||||
'<link rel="icon" sizes="16x16" href="/favicon.ico"><link rel="shortcut icon" sizes="16x16" href="/favicon.ico"><link rel="icon" sizes="192x192" href="/manifest-icon-192x192.png"><link rel="apple-touch-icon" sizes="256x256" href="/apple-touch-icon.png">';
|
||||
|
||||
updateFavicon('not-found');
|
||||
|
||||
expect(document.querySelectorAll('link[rel="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"]')).toHaveLength(4);
|
||||
expect(document.querySelector('link[rel="icon"][sizes="16x16"]')?.getAttribute('href')).toBe('/favicon-404.ico?variant=404');
|
||||
expect(document.querySelector('link[rel="shortcut icon"][sizes="16x16"]')?.getAttribute('href')).toBe('/favicon-404.ico?variant=404');
|
||||
expect(document.querySelector('link[rel="icon"][sizes="16x16"]')?.getAttribute('type')).toBe('image/x-icon');
|
||||
expect(document.querySelector('link[rel="shortcut icon"][sizes="16x16"]')?.getAttribute('type')).toBe('image/x-icon');
|
||||
expect(document.querySelector('link[rel="icon"][sizes="192x192"]')?.getAttribute('href')).toBe('/manifest-icon-192x192.png');
|
||||
expect(document.querySelector('link[rel="apple-touch-icon"]')?.getAttribute('href')).toBe('/apple-touch-icon.png');
|
||||
});
|
||||
|
||||
it('marks only non-special, non-routing aggregate sfw boards as sfw', async () => {
|
||||
const { isSfwBoard } = await import('../update-favicon');
|
||||
|
||||
|
||||
@@ -1,35 +1,50 @@
|
||||
const DEFAULT_FAVICON = '/favicon.ico?variant=nsfw';
|
||||
const SFW_FAVICON = '/favicon2.ico?variant=sfw';
|
||||
const NOT_FOUND_FAVICON = '/favicon-404.ico?variant=404';
|
||||
const FAVICON_RELS = ['icon', 'shortcut icon'] as const;
|
||||
const FAVICON_SELECTOR = ['link[data-fivechan-tab-favicon="true"]', ...FAVICON_RELS.map((rel) => `link[rel="${rel}"][sizes="16x16"]`)].join(', ');
|
||||
|
||||
type FaviconVariant = 'default' | 'sfw' | 'not-found';
|
||||
|
||||
const FAVICONS: Record<FaviconVariant, { href: string; type: string }> = {
|
||||
default: { href: DEFAULT_FAVICON, type: 'image/png' },
|
||||
sfw: { href: SFW_FAVICON, type: 'image/png' },
|
||||
'not-found': { href: NOT_FOUND_FAVICON, type: 'image/x-icon' },
|
||||
};
|
||||
|
||||
let currentHref: string | null = null;
|
||||
|
||||
const hasExpectedFaviconLinks = (href: string): boolean =>
|
||||
FAVICON_RELS.every((rel) => document.querySelector(`link[rel="${rel}"][href="${href}"][data-fivechan-tab-favicon="true"]`));
|
||||
|
||||
const createFaviconLink = (rel: (typeof FAVICON_RELS)[number], href: string): HTMLLinkElement => {
|
||||
const createFaviconLink = (rel: (typeof FAVICON_RELS)[number], favicon: (typeof FAVICONS)[FaviconVariant]): HTMLLinkElement => {
|
||||
const link = document.createElement('link');
|
||||
link.rel = rel;
|
||||
link.type = 'image/png';
|
||||
link.type = favicon.type;
|
||||
link.setAttribute('sizes', '16x16');
|
||||
link.href = href;
|
||||
link.href = favicon.href;
|
||||
link.dataset.fivechanTabFavicon = 'true';
|
||||
return link;
|
||||
};
|
||||
|
||||
const getFaviconVariant = (variant: boolean | FaviconVariant): FaviconVariant => {
|
||||
if (typeof variant === 'boolean') return variant ? 'sfw' : 'default';
|
||||
return variant;
|
||||
};
|
||||
|
||||
/**
|
||||
* Swap the tab favicon between the default (NSFW/home) and SFW variants.
|
||||
* Swap the tab favicon between the default (NSFW/home), SFW, and 404 variants.
|
||||
* 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;
|
||||
export const updateFavicon = (variant: boolean | FaviconVariant): void => {
|
||||
const favicon = FAVICONS[getFaviconVariant(variant)];
|
||||
const { href } = favicon;
|
||||
if (href === currentHref && hasExpectedFaviconLinks(href)) return;
|
||||
currentHref = href;
|
||||
|
||||
document.querySelectorAll<HTMLLinkElement>(FAVICON_SELECTOR).forEach((link) => link.remove());
|
||||
FAVICON_RELS.forEach((rel) => {
|
||||
document.head.appendChild(createFaviconLink(rel, href));
|
||||
document.head.appendChild(createFaviconLink(rel, favicon));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,14 @@ describe('view-utils', () => {
|
||||
expect(isModQueueView('/music.eth/mod/queue')).toBe(true);
|
||||
expect(isSubscriptionsView('/subs/catalog/settings', {})).toBe(true);
|
||||
expect(isPendingPostView('/pending/42/settings', { accountCommentIndex: '42' })).toBe(true);
|
||||
expect(isNotFoundView('/faq', {})).toBe(false);
|
||||
expect(isNotFoundView('/pass', {})).toBe(false);
|
||||
expect(isNotFoundView('/rules', {})).toBe(false);
|
||||
expect(isNotFoundView('/blotter', {})).toBe(false);
|
||||
expect(isNotFoundView('/settings/account-data', {})).toBe(false);
|
||||
expect(isNotFoundView('/not-allowed', {})).toBe(false);
|
||||
expect(isNotFoundView('/not-found', {})).toBe(true);
|
||||
expect(isNotFoundView('/faq/missing', {})).toBe(true);
|
||||
});
|
||||
|
||||
it('detects board, catalog, post, and settings routes using board params', () => {
|
||||
|
||||
@@ -6,6 +6,10 @@ type ParamsType = {
|
||||
commentCid?: string;
|
||||
};
|
||||
|
||||
const STATIC_APP_ROUTES = new Set(['/faq', '/pass', '/rules', '/blotter', '/settings/account-data', '/not-allowed']);
|
||||
|
||||
const isStaticAppRoute = (pathname: string): boolean => STATIC_APP_ROUTES.has(pathname);
|
||||
|
||||
export const isAllView = (pathname: string): boolean => {
|
||||
return pathname.startsWith('/all');
|
||||
};
|
||||
@@ -97,6 +101,7 @@ export const isNotFoundView = (pathname: string, params: ParamsType): boolean =>
|
||||
!isArchiveView(pathname, params) &&
|
||||
!isCatalogView(pathname, params) &&
|
||||
!isHomeView(pathname) &&
|
||||
!isStaticAppRoute(pathname) &&
|
||||
!isPendingPostView(pathname, params) &&
|
||||
!isPostPageView(pathname, params) &&
|
||||
!isSettingsView(pathname, params) &&
|
||||
|
||||
+3
-1
@@ -39,6 +39,7 @@ const neverPrecacheUrls = new Set(['index.html', 'version.json']);
|
||||
const vitePwaManagedAssetUrls = new Set([
|
||||
'manifest.webmanifest',
|
||||
'favicon.ico',
|
||||
'favicon-404.ico',
|
||||
'favicon2.ico',
|
||||
'robots.txt',
|
||||
'apple-touch-icon.png',
|
||||
@@ -50,6 +51,7 @@ const baselineAppShellUrls = new Set([
|
||||
'manifest.json',
|
||||
'manifest.webmanifest',
|
||||
'favicon.ico',
|
||||
'favicon-404.ico',
|
||||
'favicon2.ico',
|
||||
'robots.txt',
|
||||
'apple-touch-icon.png',
|
||||
@@ -393,7 +395,7 @@ export default defineConfig({
|
||||
enabled: true,
|
||||
type: 'module',
|
||||
},
|
||||
includeAssets: ['favicon.ico', 'favicon2.ico', 'robots.txt', 'apple-touch-icon.png'],
|
||||
includeAssets: ['favicon.ico', 'favicon-404.ico', 'favicon2.ico', 'robots.txt', 'apple-touch-icon.png'],
|
||||
manifest: {
|
||||
name: '5chan',
|
||||
short_name: '5chan',
|
||||
|
||||
Reference in New Issue
Block a user