From 95088300bda37ecc5df0e278d5d067f048fe6b1d Mon Sep 17 00:00:00 2001 From: plebeius Date: Sat, 7 Mar 2026 16:02:19 +0800 Subject: [PATCH] refactor(routing): namespace board mod queue under board mod routes Updated the canonical board mod queue URL to /:boardIdentifier/mod/queue and made legacy /:boardIdentifier/modqueue paths resolve to not found. Tightened route helpers so board-scoped mod URLs no longer fall through as generic board pages. --- src/app.tsx | 44 ++++++++++++++++++--- src/lib/utils/__tests__/route-utils.test.ts | 39 +++++++++++++++++- src/lib/utils/route-utils.ts | 37 +++++++++++++++-- src/lib/utils/url-utils.ts | 3 +- src/lib/utils/view-utils.ts | 5 ++- src/views/mod-queue/mod-queue.tsx | 6 +-- 6 files changed, 118 insertions(+), 16 deletions(-) diff --git a/src/app.tsx b/src/app.tsx index bb2e65b9..2d56f761 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -12,7 +12,18 @@ import useIsMobile from './hooks/use-is-mobile'; import useTheme from './hooks/use-theme'; import { useDirectories } from './hooks/use-directories'; import { useResolvedSubplebbitAddress } from './hooks/use-resolved-subplebbit-address'; -import { getBoardPath, getSubplebbitAddress, isDirectoryBoard, isPostRoute, isPendingPostRoute, isModQueueRoute } from './lib/utils/route-utils'; +import { + getBoardPath, + getSubplebbitAddress, + isBoardModRoute, + isDirectoryBoard, + isLegacyBoardModQueueRoute, + isPostRoute, + isPendingPostRoute, + isModQueueRoute, + isValidBoardModRoute, + isValidModRoute, +} from './lib/utils/route-utils'; import styles from './app.module.css'; import { DesktopBoardButtons, MobileBoardButtons } from './components/board-buttons'; import Board from './views/board'; @@ -90,6 +101,20 @@ const BoardLayout = () => { return ; } + // Invalid /mod/ paths (e.g. /mod/modqueue, /mod/asdoijasd) -> not-found + if (location.pathname.startsWith('/mod/') && !isValidModRoute(location.pathname)) { + return ; + } + + if (isLegacyBoardModQueueRoute(location.pathname)) { + return ; + } + + // Invalid board-scoped mod paths (e.g. /biz/mod, /biz/mod/asdoijasd) -> not-found + if (isBoardModRoute(location.pathname) && !isValidBoardModRoute(location.pathname)) { + return ; + } + // Normalize address URLs to directory codes: /anime-and-manga.eth/thread/xxx -> /a/thread/xxx if (boardIdentifier && !isDirectoryBoard(boardIdentifier, directories)) { const canonicalBoardIdentifier = getBoardPath(boardIdentifier, directories); @@ -287,10 +312,12 @@ const App = () => { - } /> - } /> + } /> + } /> - {/* Invalid subpaths: old time-filter URLs (e.g. /all/24h) -> not-found */} + {/* Invalid subpaths: old URLs and unknown paths -> not-found */} + } /> + } /> } /> } /> } /> @@ -302,8 +329,13 @@ const App = () => { - } /> - } /> + } /> + } /> + + } /> + } /> + } /> + } /> } /> } /> diff --git a/src/lib/utils/__tests__/route-utils.test.ts b/src/lib/utils/__tests__/route-utils.test.ts index a480aae8..815a623d 100644 --- a/src/lib/utils/__tests__/route-utils.test.ts +++ b/src/lib/utils/__tests__/route-utils.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { isFeedRoute, normalizeMultiboardFeedPath } from '../route-utils'; +import { isBoardModRoute, isFeedRoute, isLegacyBoardModQueueRoute, isModQueueRoute, isValidBoardModRoute, normalizeMultiboardFeedPath } from '../route-utils'; describe('normalizeMultiboardFeedPath', () => { it('normalizes /all/3 -> /all', () => { @@ -41,4 +41,41 @@ describe('isFeedRoute', () => { expect(isFeedRoute('/subs/catalog/1w')).toBe(false); expect(isFeedRoute('/all/1w/3')).toBe(false); }); + + it('returns false for board-scoped mod namespace paths', () => { + expect(isFeedRoute('/biz/mod')).toBe(false); + expect(isFeedRoute('/biz/mod/queue')).toBe(false); + }); +}); + +describe('board mod routes', () => { + it('recognizes canonical mod queue routes', () => { + expect(isModQueueRoute('/mod/queue')).toBe(true); + expect(isModQueueRoute('/biz/mod/queue')).toBe(true); + expect(isModQueueRoute('/biz/mod/queue/settings')).toBe(true); + }); + + it('does not recognize legacy board modqueue routes', () => { + expect(isModQueueRoute('/biz/modqueue')).toBe(false); + }); + + it('recognizes legacy board modqueue routes for rejection', () => { + expect(isLegacyBoardModQueueRoute('/biz/modqueue')).toBe(true); + expect(isLegacyBoardModQueueRoute('/biz/modqueue/settings')).toBe(true); + expect(isLegacyBoardModQueueRoute('/biz/mod/queue')).toBe(false); + }); + + it('recognizes board mod namespace paths', () => { + expect(isBoardModRoute('/biz/mod')).toBe(true); + expect(isBoardModRoute('/biz/mod/queue')).toBe(true); + expect(isBoardModRoute('/mod/queue')).toBe(false); + }); + + it('validates allowed board mod routes', () => { + expect(isValidBoardModRoute('/biz/mod/queue')).toBe(true); + expect(isValidBoardModRoute('/biz/mod/queue/settings')).toBe(true); + expect(isValidBoardModRoute('/biz/mod')).toBe(false); + expect(isValidBoardModRoute('/biz/mod/log')).toBe(false); + expect(isValidBoardModRoute('/biz/modqueue')).toBe(false); + }); }); diff --git a/src/lib/utils/route-utils.ts b/src/lib/utils/route-utils.ts index 2683c24a..5ae7ca12 100644 --- a/src/lib/utils/route-utils.ts +++ b/src/lib/utils/route-utils.ts @@ -120,7 +120,7 @@ export const isFeedRoute = (pathname: string): boolean => { if (normalizedPath.includes('/thread/')) return false; if (normalizedPath.startsWith('/pending/')) return false; - if (normalizedPath.includes('/modqueue')) return false; + if (isBoardModRoute(normalizedPath) || isModQueueRoute(normalizedPath)) return false; const pathWithoutSettings = normalizedPath.replace(/\/settings$/, ''); const segments = pathWithoutSettings.split('/').filter(Boolean); @@ -147,9 +147,38 @@ export const isPendingPostRoute = (pathname: string): boolean => { return normalizedPath.startsWith('/pending/'); }; +export const isBoardModRoute = (pathname: string): boolean => { + const normalizedPath = pathname.replace(/\/$/, ''); + return /^\/[^/]+\/mod(?:\/.*)?$/.test(normalizedPath); +}; + +export const isLegacyBoardModQueueRoute = (pathname: string): boolean => { + const normalizedPath = pathname.replace(/\/$/, ''); + return /^\/[^/]+\/modqueue(?:\/settings)?$/.test(normalizedPath); +}; + export const isModQueueRoute = (pathname: string): boolean => { - const normalizedPath = pathname.replace(/\/settings$/, ''); - return normalizedPath.includes('/modqueue'); + const normalizedPath = pathname.replace(/\/settings$/, '').replace(/\/$/, ''); + return normalizedPath === '/mod/queue' || /^\/[^/]+\/mod\/queue$/.test(normalizedPath); +}; + +const VALID_MOD_PATHS = ['/mod', '/mod/settings', '/mod/catalog', '/mod/catalog/settings', '/mod/queue', '/mod/queue/settings']; +const VALID_BOARD_MOD_SUBPATHS = ['queue', 'queue/settings']; + +export const isValidModRoute = (pathname: string): boolean => { + const normalized = pathname.replace(/\/$/, ''); + return VALID_MOD_PATHS.includes(normalized); +}; + +export const isValidBoardModRoute = (pathname: string): boolean => { + const normalizedPath = pathname.replace(/\/$/, ''); + const match = normalizedPath.match(/^\/[^/]+\/mod(?:\/(.*))?$/); + if (!match) { + return false; + } + + const subpath = match[1] ?? ''; + return VALID_BOARD_MOD_SUBPATHS.includes(subpath); }; /** Page numbers 1–10 for board feed pagination */ @@ -228,7 +257,7 @@ export const getFeedCacheKey = (pathname: string): string | null => { return null; } - if (normalizedPath.includes('/modqueue')) { + if (isBoardModRoute(normalizedPath) || isModQueueRoute(normalizedPath)) { return null; } diff --git a/src/lib/utils/url-utils.ts b/src/lib/utils/url-utils.ts index c817e166..f5323ce2 100644 --- a/src/lib/utils/url-utils.ts +++ b/src/lib/utils/url-utils.ts @@ -82,10 +82,11 @@ export const is5chanLink = (url: string): boolean => { // - /{boardIdentifier} (directory code or address) // - /{boardIdentifier}/thread/{commentCid} // - /{boardIdentifier}/catalog + // - /{boardIdentifier}/mod/queue // - /all, /subs, /mod, /pending/{index} return ( /^\/p\/[^/]+(\/c\/[^/]+)?$/.test(routePath) || - /^\/[^/]+(\/thread\/[^/]+|\/catalog)?$/.test(routePath) || + /^\/[^/]+(\/thread\/[^/]+|\/catalog|\/mod\/queue)?$/.test(routePath) || /^\/(all|subscriptions|mod)(\/catalog|\/thread\/[^/]+)?(\/[^/]+)?$/.test(routePath) || /^\/pending\/[^/]+$/.test(routePath) ); diff --git a/src/lib/utils/view-utils.ts b/src/lib/utils/view-utils.ts index dae8f71b..ef29a79f 100644 --- a/src/lib/utils/view-utils.ts +++ b/src/lib/utils/view-utils.ts @@ -1,3 +1,5 @@ +import { isBoardModRoute, isModQueueRoute } from './route-utils'; + export type ParamsType = { accountCommentIndex?: string; boardIdentifier?: string; @@ -17,6 +19,7 @@ export const isBoardView = (pathname: string, params: ParamsType): boolean => { pathname.startsWith('/all') || pathname.startsWith('/subs') || pathname.startsWith('/mod') || + isBoardModRoute(pathname) || pathname.startsWith('/pending') || pathname === '/' || pathname.startsWith('/faq') || @@ -53,7 +56,7 @@ export const isModView = (pathname: string): boolean => { }; export const isModQueueView = (pathname: string): boolean => { - return pathname.includes('/modqueue'); + return isModQueueRoute(pathname); }; export const isPendingPostView = (pathname: string, params: ParamsType): boolean => { diff --git a/src/views/mod-queue/mod-queue.tsx b/src/views/mod-queue/mod-queue.tsx index 61858607..6a6aa50d 100644 --- a/src/views/mod-queue/mod-queue.tsx +++ b/src/views/mod-queue/mod-queue.tsx @@ -269,7 +269,7 @@ const ModQueueRow = memo(({ comment, isOdd = false, showBoard = false, boardPath const threadTargetCid = threadCid || cid; const postUrl = boardPath && threadTargetCid ? `/${boardPath}/thread/${threadTargetCid}` : undefined; - const modQueueUrl = boardPath ? `/${boardPath}/modqueue` : undefined; + const modQueueUrl = boardPath ? `/${boardPath}/mod/queue` : undefined; return (
@@ -370,7 +370,7 @@ const ModQueueCard = memo(({ comment, showBoard = false, boardPath }: ModQueueCa const threadTargetCid = threadCid || cid; const postUrl = boardPath && threadTargetCid ? `/${boardPath}/thread/${threadTargetCid}` : undefined; - const modQueueUrl = boardPath ? `/${boardPath}/modqueue` : undefined; + const modQueueUrl = boardPath ? `/${boardPath}/mod/queue` : undefined; return (
@@ -651,7 +651,7 @@ const ModQueueButtonContent = ({ feed, alertThresholdSeconds, boardIdentifier, i }, [statusMap]); const totalCount = normalCount + urgentCount; - const to = boardIdentifier ? `/${boardIdentifier}/modqueue` : '/mod/modqueue'; + const to = boardIdentifier ? `/${boardIdentifier}/mod/queue` : '/mod/queue'; const buttonContent = (