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 = (