fix(feeds): force infinite scroll on multiboards and canonicalize page URLs

This commit is contained in:
plebeius
2026-02-22 14:24:45 +08:00
parent 445b271982
commit 71fdbef1ea
42 changed files with 231 additions and 69 deletions
@@ -0,0 +1,29 @@
import { describe, it, expect } from 'vitest';
import { normalizeMultiboardFeedPath } from '../route-utils';
describe('normalizeMultiboardFeedPath', () => {
it('normalizes /all/3 -> /all', () => {
expect(normalizeMultiboardFeedPath('/all/3')).toBe('/all');
});
it('normalizes /all/1w/3 -> /all/1w', () => {
expect(normalizeMultiboardFeedPath('/all/1w/3')).toBe('/all/1w');
});
it('normalizes /subs/2/settings -> /subs/settings', () => {
expect(normalizeMultiboardFeedPath('/subs/2/settings')).toBe('/subs/settings');
});
it('normalizes /mod/catalog/4 -> /mod/catalog', () => {
expect(normalizeMultiboardFeedPath('/mod/catalog/4')).toBe('/mod/catalog');
});
it('leaves non-multiboard paths unchanged', () => {
expect(normalizeMultiboardFeedPath('/biz')).toBe('/biz');
expect(normalizeMultiboardFeedPath('/biz/3')).toBe('/biz/3');
expect(normalizeMultiboardFeedPath('/biz/1w/2')).toBe('/biz/1w/2');
expect(normalizeMultiboardFeedPath('/biz/catalog/4')).toBe('/biz/catalog/4');
expect(normalizeMultiboardFeedPath('/pending/0')).toBe('/pending/0');
expect(normalizeMultiboardFeedPath('/')).toBe('/');
});
});
+37
View File
@@ -150,6 +150,43 @@ export const BOARD_PAGE_REGEX = /^([1-9]|10)$/;
export const isBoardFeedPageNumber = (segment: string): boolean => BOARD_PAGE_REGEX.test(segment);
/** Internal: check if segment is a multiboard root (all, subs, mod) */
function isMultiboardRoot(segment: string): boolean {
return segment === 'all' || segment === 'subs' || segment === 'mod';
}
/** Internal: check if pathname is a multiboard feed path (starts with /all, /subs, or /mod) */
function isMultiboardFeedPath(pathname: string): boolean {
const trimmed = pathname.replace(/\/$/, '');
const segments = trimmed.split('/').filter(Boolean);
return segments.length > 0 && isMultiboardRoot(segments[0]);
}
/**
* Normalize multiboard feed paths by removing trailing page-number segments (110),
* while preserving /settings and valid time-filter segments.
* Non-multiboard paths are returned unchanged.
*/
export const normalizeMultiboardFeedPath = (pathname: string): string => {
let path = pathname.replace(/\/$/, '');
if (!isMultiboardFeedPath(path)) {
return pathname;
}
const hasSettings = path.endsWith('/settings');
if (hasSettings) {
path = path.replace(/\/settings$/, '');
}
const segments = path.split('/').filter(Boolean);
if (segments.length > 1 && isBoardFeedPageNumber(segments[segments.length - 1])) {
const newPath = '/' + segments.slice(0, -1).join('/');
return hasSettings ? newPath + '/settings' : newPath;
}
return hasSettings ? path + '/settings' : path;
};
/** Strip trailing page number (110) from path for feed cache key */
export const stripPageFromFeedPath = (path: string): string => {
const segments = path.split('/').filter(Boolean);