Repository: IgnisDa/ryot (language: rust)

Issue:
Exercise Page: Exercises don't show if already on page not populatable by search results
Say if you're on page 20 and search for dumbell, there will be zero results displayed as the total results for dumbell are 4 pages and search seems to remain at page 20.
Pressing on available page numbers (1,2,3,4) will give you the results you desire.

**Expectation**
Automatically reset to page 1 upon searching and display available items.


Discussion / hints:

Fix patch:
diff --git a/apps/frontend/app/lib/generals.ts b/apps/frontend/app/lib/generals.ts
index 9b8664d432..e1638657af 100644
--- a/apps/frontend/app/lib/generals.ts
+++ b/apps/frontend/app/lib/generals.ts
@@ -60,6 +60,7 @@ export const CurrentWorkoutKey = "CurrentWorkout";
 export const LOGO_IMAGE_URL =
 	"https://raw.githubusercontent.com/IgnisDa/ryot/main/libs/assets/icon-512x512.png";
 export const redirectToQueryParam = "redirectTo";
+export const pageQueryParam = "page";
 export const AUTH_COOKIE_NAME = "Auth";
 export const toastKey = "Toast";
 
diff --git a/apps/frontend/app/lib/utilities.server.ts b/apps/frontend/app/lib/utilities.server.ts
index b3ef4f2f41..07cc71b048 100644
--- a/apps/frontend/app/lib/utilities.server.ts
+++ b/apps/frontend/app/lib/utilities.server.ts
@@ -34,6 +34,7 @@ import {
 	AUTH_COOKIE_NAME,
 	CurrentWorkoutKey,
 	dayjsLib,
+	pageQueryParam,
 	queryClient,
 	queryFactory,
 	redirectToQueryParam,
@@ -397,19 +398,34 @@ export const isWorkoutActive = (request: Request) => {
 	return inProgress;
 };
 
+export const getEnhancedCookieName = async (path: string, request: Request) => {
+	const userDetails = await redirectIfNotAuthenticatedOrUpdated(request);
+	return `SearchParams__${userDetails.id}__${path}`;
+};
+
 export const redirectUsingEnhancedCookieSearchParams = async (
 	request: Request,
 	cookieName: string,
 ) => {
 	const preferences = await getCachedUserPreferences(request);
-	const searchParams = new URL(request.url).searchParams;
+	const { searchParams } = new URL(request.url);
 	if (searchParams.size > 0 || !preferences.general.persistQueries) return;
 	const cookies = parse(request.headers.get("cookie") || "");
 	const savedSearchParams = cookies[cookieName];
 	if (!isEmpty(savedSearchParams)) throw redirect(`?${savedSearchParams}`);
 };
 
-export const getEnhancedCookieName = async (path: string, request: Request) => {
-	const userDetails = await redirectIfNotAuthenticatedOrUpdated(request);
-	return `SearchParams__${userDetails.id}__${path}`;
+export const redirectToFirstPageIfOnInvalidPage = async (
+	request: Request,
+	totalResults: number,
+	currentPage: number,
+) => {
+	const { coreDetails } = await getCachedCoreDetails();
+	const { searchParams } = new URL(request.url);
+	const totalPages = Math.ceil(totalResults / coreDetails.pageLimit);
+	if (currentPage > totalPages && currentPage !== 1) {
+		searchParams.set(pageQueryParam, "1");
+		throw redirect(`?${searchParams.toString()}`);
+	}
+	return totalPages;
 };
diff --git a/apps/frontend/app/routes/_dashboard.collections.$id._index.tsx b/apps/frontend/app/routes/_dashboard.collections.$id._index.tsx
index 7fdabc4984..b8080492f6 100644
--- a/apps/frontend/app/routes/_dashboard.collections.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.collections.$id._index.tsx
@@ -57,17 +57,15 @@ import {
 import {
 	clientGqlService,
 	dayjsLib,
+	pageQueryParam,
 	queryClient,
 	queryFactory,
 } from "~/lib/generals";
-import {
-	useAppSearchParam,
-	useCoreDetails,
-	useUserPreferences,
-} from "~/lib/hooks";
+import { useAppSearchParam, useUserPreferences } from "~/lib/hooks";
 import { useReviewEntity } from "~/lib/state/media";
 import {
 	getEnhancedCookieName,
+	redirectToFirstPageIfOnInvalidPage,
 	redirectUsingEnhancedCookieSearchParams,
 	removeCachedUserCollectionsList,
 	serverGqlService,
@@ -82,7 +80,7 @@ const defaultFiltersValue = {
 
 const searchParamsSchema = z.object({
 	defaultTab: z.string().optional(),
-	page: zx.IntAsString.optional(),
+	[pageQueryParam]: zx.IntAsString.optional(),
 	query: z.string().optional(),
 	sortBy: z
 		.nativeEnum(CollectionContentsSortBy)
@@ -111,11 +109,16 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 					metadataLot: query.metadataLot,
 				},
 				sort: { by: query.sortBy, order: query.orderBy },
-				search: { page: query.page, query: query.query },
+				search: { page: query[pageQueryParam], query: query.query },
 			},
 		}),
 	]);
-	return { collectionId, query, collectionContents, cookieName };
+	const totalPages = await redirectToFirstPageIfOnInvalidPage(
+		request,
+		collectionContents.results.details.total,
+		query[pageQueryParam] || 1,
+	);
+	return { collectionId, query, collectionContents, cookieName, totalPages };
 });
 
 export const meta = ({ data }: MetaArgs_SingleFetch<typeof loader>) => {
@@ -160,7 +163,6 @@ const bulkRemoveSchema = z.object({
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
 	const userPreferences = useUserPreferences();
-	const coreDetails = useCoreDetails();
 	const [tab, setTab] = useState<string | null>(
 		loaderData.query.defaultTab || DEFAULT_TAB,
 	);
@@ -364,12 +366,9 @@ export default function Page() {
 								<Center>
 									<Pagination
 										size="sm"
-										value={loaderData.query.page}
-										onChange={(v) => setP("page", v.toString())}
-										total={Math.ceil(
-											loaderData.collectionContents.results.details.total /
-												coreDetails.pageLimit,
-										)}
+										total={loaderData.totalPages}
+										value={loaderData.query[pageQueryParam]}
+										onChange={(v) => setP(pageQueryParam, v.toString())}
 									/>
 								</Center>
 							) : null}
diff --git a/apps/frontend/app/routes/_dashboard.fitness.$entity.list.tsx b/apps/frontend/app/routes/_dashboard.fitness.$entity.list.tsx
index 284f199783..5181dd268d 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.$entity.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.$entity.list.tsx
@@ -41,22 +41,22 @@ import {
 	displayWeightWithUnit,
 	getSetStatisticsTextToDisplay,
 } from "~/components/fitness";
-import { dayjsLib } from "~/lib/generals";
+import { dayjsLib, pageQueryParam } from "~/lib/generals";
 import {
 	useAppSearchParam,
-	useCoreDetails,
 	useGetWorkoutStarter,
 	useUserUnitSystem,
 } from "~/lib/hooks";
 import { getDefaultWorkout } from "~/lib/state/fitness";
 import {
 	getEnhancedCookieName,
+	redirectToFirstPageIfOnInvalidPage,
 	redirectUsingEnhancedCookieSearchParams,
 	serverGqlService,
 } from "~/lib/utilities.server";
 
 const searchParamsSchema = z.object({
-	page: zx.IntAsString.default("1"),
+	[pageQueryParam]: zx.IntAsString.default("1"),
 	query: z.string().optional(),
 });
 
@@ -76,7 +76,7 @@ export const loader = unstable_defineLoader(async ({ params, request }) => {
 			const { userWorkoutsList } = await serverGqlService.authenticatedRequest(
 				request,
 				UserWorkoutsListDocument,
-				{ input: { page: query.page, query: query.query } },
+				{ input: { page: query[pageQueryParam], query: query.query } },
 			);
 			return {
 				details: userWorkoutsList.details,
@@ -96,7 +96,12 @@ export const loader = unstable_defineLoader(async ({ params, request }) => {
 			};
 		})
 		.exhaustive();
-	return { query, entity, itemList, cookieName };
+	const totalPages = await redirectToFirstPageIfOnInvalidPage(
+		request,
+		itemList.details.total,
+		query[pageQueryParam],
+	);
+	return { query, entity, itemList, cookieName, totalPages };
 });
 
 export const meta = ({ data }: MetaArgs_SingleFetch<typeof loader>) => {
@@ -105,7 +110,6 @@ export const meta = ({ data }: MetaArgs_SingleFetch<typeof loader>) => {
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
-	const coreDetails = useCoreDetails();
 	const [_, { setP }] = useAppSearchParam(loaderData.cookieName);
 	const startWorkout = useGetWorkoutStarter();
 	const unitSystem = useUserUnitSystem();
@@ -221,11 +225,9 @@ export default function Page() {
 				<Center>
 					<Pagination
 						size="sm"
-						value={loaderData.query.page}
-						onChange={(v) => setP("page", v.toString())}
-						total={Math.ceil(
-							loaderData.itemList.details.total / coreDetails.pageLimit,
-						)}
+						total={loaderData.totalPages}
+						value={loaderData.query[pageQueryParam]}
+						onChange={(v) => setP(pageQueryParam, v.toString())}
 					/>
 				</Center>
 			</Stack>
diff --git a/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx b/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx
index 3b566e050f..fe31e47de3 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx
@@ -49,17 +49,14 @@ import { $path } from "remix-routes";
 import { z } from "zod";
 import { zx } from "zodix";
 import { DebouncedSearchInput, FiltersModal } from "~/components/common";
-import { dayjsLib } from "~/lib/generals";
-import {
-	useAppSearchParam,
-	useCoreDetails,
-	useUserCollections,
-} from "~/lib/hooks";
+import { dayjsLib, pageQueryParam } from "~/lib/generals";
+import { useAppSearchParam, useUserCollections } from "~/lib/hooks";
 import { addExerciseToWorkout, useCurrentWorkout } from "~/lib/state/fitness";
 import {
 	getCachedExerciseParameters,
 	getEnhancedCookieName,
 	getWorkoutCookieValue,
+	redirectToFirstPageIfOnInvalidPage,
 	redirectUsingEnhancedCookieSearchParams,
 	serverGqlService,
 } from "~/lib/utilities.server";
@@ -76,7 +73,7 @@ const defaultFiltersValue = {
 };
 
 const searchParamsSchema = z.object({
-	page: zx.IntAsString.optional(),
+	[pageQueryParam]: zx.IntAsString.optional(),
 	query: z.string().optional(),
 	sortBy: z.nativeEnum(ExerciseSortBy).optional(),
 	type: z.nativeEnum(ExerciseLot).optional(),
@@ -96,31 +93,41 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 	const query = zx.parseQuery(request, searchParamsSchema);
 	const workoutInProgress = !!getWorkoutCookieValue(request);
 	query.sortBy = query.sortBy ?? defaultFiltersValue.sortBy;
-	query.page = query.page ?? 1;
+	query[pageQueryParam] = query[pageQueryParam] ?? 1;
 	const [exerciseParameters, { exercisesList }] = await Promise.all([
 		getCachedExerciseParameters(),
-		serverGqlService.authenticatedRequest(request, ExercisesListDocument, {
-			input: {
-				search: { page: query.page, query: query.query },
-				filter: {
-					equipment: query.equipment,
-					force: query.force,
-					level: query.level,
-					mechanic: query.mechanic,
-					muscle: query.muscle,
-					type: query.type,
-					collection: query.collection,
+		serverGqlService.authenticatedRequest(
+			request.clone(),
+			ExercisesListDocument,
+			{
+				input: {
+					search: { page: query[pageQueryParam], query: query.query },
+					filter: {
+						equipment: query.equipment,
+						force: query.force,
+						level: query.level,
+						mechanic: query.mechanic,
+						muscle: query.muscle,
+						type: query.type,
+						collection: query.collection,
+					},
+					sortBy: query.sortBy,
 				},
-				sortBy: query.sortBy,
 			},
-		}),
+		),
 	]);
+	const totalPages = await redirectToFirstPageIfOnInvalidPage(
+		request,
+		exercisesList.details.total,
+		query[pageQueryParam],
+	);
 	return {
 		query,
+		totalPages,
+		cookieName,
+		exercisesList,
 		workoutInProgress,
 		exerciseParameters,
-		exercisesList,
-		cookieName,
 	};
 });
 
@@ -130,7 +137,6 @@ export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
-	const coreDetails = useCoreDetails();
 	const navigate = useNavigate();
 	const [_, { setP }] = useAppSearchParam(loaderData.cookieName);
 	const [selectedExercises, setSelectedExercises] = useListState<{
@@ -145,7 +151,7 @@ export default function Page() {
 	const [currentWorkout, setCurrentWorkout] = useCurrentWorkout();
 
 	const isFilterChanged = Object.keys(defaultFiltersValue)
-		.filter((k) => k !== "page" && k !== "query")
+		.filter((k) => k !== pageQueryParam && k !== "query")
 		.some(
 			// biome-ignore lint/suspicious/noExplicitAny: required here
 			(k) => (loaderData.query as any)[k] !== (defaultFiltersValue as any)[k],
@@ -287,19 +293,14 @@ export default function Page() {
 						) : (
 							<Text>No information to display</Text>
 						)}
-						{loaderData.exercisesList.details.total > 0 ? (
-							<Center>
-								<Pagination
-									size="sm"
-									value={loaderData.query.page}
-									onChange={(v) => setP("page", v.toString())}
-									total={Math.ceil(
-										loaderData.exercisesList.details.total /
-											coreDetails.pageLimit,
-									)}
-								/>
-							</Center>
-						) : null}
+						<Center>
+							<Pagination
+								size="sm"
+								value={loaderData.query[pageQueryParam]}
+								onChange={(v) => setP(pageQueryParam, v.toString())}
+								total={loaderData.totalPages}
+							/>
+						</Center>
 					</>
 				)}
 			</Stack>
diff --git a/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx b/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx
index 9a16a0eb1a..b1782560b5 100644
--- a/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx
@@ -60,11 +60,16 @@ import {
 	FiltersModal,
 } from "~/components/common";
 import { BaseMediaDisplayItem, MetadataDisplayItem } from "~/components/media";
-import { Verb, commaDelimitedString, getLot, getVerb } from "~/lib/generals";
+import {
+	Verb,
+	commaDelimitedString,
+	getLot,
+	getVerb,
+	pageQueryParam,
+} from "~/lib/generals";
 import {
 	useAppSearchParam,
 	useApplicationEvents,
-	useCoreDetails,
 	useUserDetails,
 	useUserPreferences,
 } from "~/lib/hooks";
@@ -74,6 +79,7 @@ import {
 } from "~/lib/state/media";
 import {
 	getEnhancedCookieName,
+	redirectToFirstPageIfOnInvalidPage,
 	redirectUsingEnhancedCookieSearchParams,
 	serverGqlService,
 } from "~/lib/utilities.server";
@@ -120,12 +126,11 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 		request,
 	);
 	await redirectUsingEnhancedCookieSearchParams(request, cookieName);
-	const { query, page } = zx.parseQuery(request, {
+	const query = zx.parseQuery(request, {
 		query: z.string().optional(),
-		page: zx.IntAsString.default("1"),
+		[pageQueryParam]: zx.IntAsString.default("1"),
 	});
-	const numPage = Number(page);
-	const [mediaList, mediaSearch] = await match(action)
+	const [totalResults, mediaList, mediaSearch] = await match(action)
 		.with(Action.List, async () => {
 			const urlParse = zx.parseQuery(request, {
 				sortOrder: z
@@ -144,7 +149,7 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 				{
 					input: {
 						lot,
-						search: { page: numPage, query },
+						search: { page: query[pageQueryParam], query: query.query },
 						sort: { order: urlParse.sortOrder, by: urlParse.sortBy },
 						filter: {
 							general: urlParse.generalFilter,
@@ -154,7 +159,11 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 					},
 				},
 			);
-			return [{ list: metadataList, url: urlParse }, undefined] as const;
+			return [
+				metadataList.details.total,
+				{ list: metadataList, url: urlParse },
+				undefined,
+			] as const;
 		})
 		.with(Action.Search, async () => {
 			const metadataSourcesForLot = metadataMapping[lot];
@@ -167,12 +176,13 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 				{
 					input: {
 						lot,
-						search: { page, query },
+						search: { page: query[pageQueryParam], query: query.query },
 						source: urlParse.source,
 					},
 				},
 			);
 			return [
+				metadataSearch.details.total,
 				undefined,
 				{
 					search: metadataSearch,
@@ -183,15 +193,21 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 		})
 		.exhaustive();
 	const url = new URL(request.url);
+	const totalPages = await redirectToFirstPageIfOnInvalidPage(
+		request,
+		totalResults,
+		query[pageQueryParam],
+	);
 	return {
 		lot,
 		query,
 		action,
-		numPage,
 		mediaList,
+		totalPages,
 		cookieName,
 		mediaSearch,
 		url: withoutHost(url.href),
+		[pageQueryParam]: Number(query[pageQueryParam]),
 	};
 });
 
@@ -208,7 +224,6 @@ export const meta = ({ params }: MetaArgs_SingleFetch<typeof loader>) => {
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
 	const userPreferences = useUserPreferences();
-	const coreDetails = useCoreDetails();
 	const [_, { setP }] = useAppSearchParam(loaderData.cookieName);
 	const [
 		filtersModalOpened,
@@ -235,7 +250,11 @@ export default function Page() {
 							$path(
 								"/media/:action/:lot",
 								{ action: v, lot: loaderData.lot.toLowerCase() },
-								{ ...(loaderData.query && { query: loaderData.query }) },
+								{
+									...(loaderData.query.query && {
+										query: loaderData.query.query,
+									}),
+								},
 							),
 						);
 				}}
@@ -265,7 +284,7 @@ export default function Page() {
 					<>
 						<Group wrap="nowrap">
 							<DebouncedSearchInput
-								initialValue={loaderData.query}
+								initialValue={loaderData.query.query}
 								enhancedQueryParams={loaderData.cookieName}
 								placeholder={`Sift through your ${changeCase(
 									loaderData.lot.toLowerCase(),
@@ -310,12 +329,9 @@ export default function Page() {
 							<Center>
 								<Pagination
 									size="sm"
-									value={loaderData.numPage}
-									onChange={(v) => setP("page", v.toString())}
-									total={Math.ceil(
-										loaderData.mediaList.list.details.total /
-											coreDetails.pageLimit,
-									)}
+									total={loaderData.totalPages}
+									value={loaderData[pageQueryParam]}
+									onChange={(v) => setP(pageQueryParam, v.toString())}
 								/>
 							</Center>
 						) : null}
@@ -328,7 +344,7 @@ export default function Page() {
 								placeholder={`Sift through your ${changeCase(
 									loaderData.lot.toLowerCase(),
 								).toLowerCase()}s`}
-								initialValue={loaderData.query}
+								initialValue={loaderData.query.query}
 								enhancedQueryParams={loaderData.cookieName}
 							/>
 							{loaderData.mediaSearch.mediaSources.length > 1 ? (
@@ -378,12 +394,9 @@ export default function Page() {
 							<Center>
 								<Pagination
 									size="sm"
-									value={loaderData.numPage}
-									onChange={(v) => setP("page", v.toString())}
-									total={Math.ceil(
-										loaderData.mediaSearch.search.details.total /
-											coreDetails.pageLimit,
-									)}
+									total={loaderData.totalPages}
+									value={loaderData[pageQueryParam]}
+									onChange={(v) => setP(pageQueryParam, v.toString())}
 								/>
 							</Center>
 						) : null}
diff --git a/apps/frontend/app/routes/_dashboard.media.genre.$id._index.tsx b/apps/frontend/app/routes/_dashboard.media.genre.$id._index.tsx
index 16df903475..7a66f3ccc7 100644
--- a/apps/frontend/app/routes/_dashboard.media.genre.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.genre.$id._index.tsx
@@ -15,15 +15,17 @@ import { z } from "zod";
 import { zx } from "zodix";
 import { ApplicationGrid } from "~/components/common";
 import { MetadataDisplayItem } from "~/components/media";
-import { useAppSearchParam, useCoreDetails } from "~/lib/hooks";
+import { pageQueryParam } from "~/lib/generals";
+import { useAppSearchParam } from "~/lib/hooks";
 import {
 	getEnhancedCookieName,
+	redirectToFirstPageIfOnInvalidPage,
 	redirectUsingEnhancedCookieSearchParams,
 	serverGqlService,
 } from "~/lib/utilities.server";
 
 const searchParamsSchema = z.object({
-	page: zx.IntAsString.default("1"),
+	[pageQueryParam]: zx.IntAsString.default("1"),
 });
 
 export type SearchParams = z.infer<typeof searchParamsSchema>;
@@ -35,10 +37,15 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 	const query = zx.parseQuery(request, searchParamsSchema);
 	const [{ genreDetails }] = await Promise.all([
 		serverGqlService.request(GenreDetailsDocument, {
-			input: { genreId, page: query.page },
+			input: { genreId, page: query[pageQueryParam] },
 		}),
 	]);
-	return { query, genreDetails, cookieName };
+	const totalPages = await redirectToFirstPageIfOnInvalidPage(
+		request,
+		genreDetails.contents.details.total,
+		query[pageQueryParam],
+	);
+	return { query, genreDetails, cookieName, totalPages };
 });
 
 export const meta = ({ data }: MetaArgs_SingleFetch<typeof loader>) => {
@@ -47,7 +54,6 @@ export const meta = ({ data }: MetaArgs_SingleFetch<typeof loader>) => {
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
-	const coreDetails = useCoreDetails();
 	const [_, { setP }] = useAppSearchParam(loaderData.cookieName);
 
 	return (
@@ -65,12 +71,9 @@ export default function Page() {
 				<Center>
 					<Pagination
 						size="sm"
-						value={loaderData.query.page}
-						onChange={(v) => setP("page", v.toString())}
-						total={Math.ceil(
-							loaderData.genreDetails.contents.details.total /
-								coreDetails.pageLimit,
-						)}
+						total={loaderData.totalPages}
+						value={loaderData.query[pageQueryParam]}
+						onChange={(v) => setP(pageQueryParam, v.toString())}
 					/>
 				</Center>
 			</Stack>
diff --git a/apps/frontend/app/routes/_dashboard.media.genre.list.tsx b/apps/frontend/app/routes/_dashboard.media.genre.list.tsx
index c0b7482b53..7293c969aa 100644
--- a/apps/frontend/app/routes/_dashboard.media.genre.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.genre.list.tsx
@@ -30,20 +30,21 @@ import {
 	DebouncedSearchInput,
 	ProRequiredAlert,
 } from "~/components/common";
+import { pageQueryParam } from "~/lib/generals";
 import {
 	useAppSearchParam,
-	useCoreDetails,
 	useFallbackImageUrl,
 	useGetMantineColor,
 } from "~/lib/hooks";
 import {
 	getEnhancedCookieName,
+	redirectToFirstPageIfOnInvalidPage,
 	redirectUsingEnhancedCookieSearchParams,
 	serverGqlService,
 } from "~/lib/utilities.server";
 
 const searchParamsSchema = z.object({
-	page: zx.IntAsString.default("1"),
+	[pageQueryParam]: zx.IntAsString.default("1"),
 	query: z.string().optional(),
 });
 
@@ -55,10 +56,15 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 	const query = zx.parseQuery(request, searchParamsSchema);
 	const [{ genresList }] = await Promise.all([
 		serverGqlService.request(GenresListDocument, {
-			input: { page: query.page, query: query.query },
+			input: { page: query[pageQueryParam], query: query.query },
 		}),
 	]);
-	return { query, genresList, cookieName };
+	const totalPages = await redirectToFirstPageIfOnInvalidPage(
+		request,
+		genresList.details.total,
+		query[pageQueryParam],
+	);
+	return { query, genresList, cookieName, totalPages };
 });
 
 export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
@@ -67,7 +73,6 @@ export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
-	const coreDetails = useCoreDetails();
 	const [_, { setP }] = useAppSearchParam(loaderData.cookieName);
 
 	return (
@@ -102,11 +107,9 @@ export default function Page() {
 					<Center mt="xl">
 						<Pagination
 							size="sm"
-							value={loaderData.query.page}
-							onChange={(v) => setP("page", v.toString())}
-							total={Math.ceil(
-								loaderData.genresList.details.total / coreDetails.pageLimit,
-							)}
+							total={loaderData.totalPages}
+							value={loaderData.query[pageQueryParam]}
+							onChange={(v) => setP(pageQueryParam, v.toString())}
 						/>
 					</Center>
 				) : null}
diff --git a/apps/frontend/app/routes/_dashboard.media.groups.$action.tsx b/apps/frontend/app/routes/_dashboard.media.groups.$action.tsx
index 3a3da4d16b..b9110afbf1 100644
--- a/apps/frontend/app/routes/_dashboard.media.groups.$action.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.groups.$action.tsx
@@ -54,10 +54,11 @@ import {
 	BaseMediaDisplayItem,
 	MetadataGroupDisplayItem,
 } from "~/components/media";
-import { commaDelimitedString } from "~/lib/generals";
-import { useAppSearchParam, useCoreDetails } from "~/lib/hooks";
+import { commaDelimitedString, pageQueryParam } from "~/lib/generals";
+import { useAppSearchParam } from "~/lib/hooks";
 import {
 	getEnhancedCookieName,
+	redirectToFirstPageIfOnInvalidPage,
 	serverGqlService,
 } from "~/lib/utilities.server";
 
@@ -83,11 +84,11 @@ const SEARCH_SOURCES_ALLOWED: Partial<Record<MediaSource, MediaLot>> = {
 export const loader = unstable_defineLoader(async ({ request, params }) => {
 	const { action } = zx.parseParams(params, { action: z.nativeEnum(Action) });
 	const cookieName = await getEnhancedCookieName(`groups.${action}`, request);
-	const { query, page } = zx.parseQuery(request, {
+	const query = zx.parseQuery(request, {
 		query: z.string().optional(),
-		page: zx.IntAsString.default("1"),
+		[pageQueryParam]: zx.IntAsString.default("1"),
 	});
-	const [list, search] = await match(action)
+	const [totalResults, list, search] = await match(action)
 		.with(Action.List, async () => {
 			const urlParse = zx.parseQuery(request, {
 				sortBy: z.nativeEnum(PersonSortBy).default(defaultFilters.sortBy),
@@ -101,14 +102,18 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 					MetadataGroupsListDocument,
 					{
 						input: {
-							search: { page, query },
+							search: { page: query[pageQueryParam], query: query.query },
 							sort: { by: urlParse.sortBy, order: urlParse.orderBy },
 							filter: { collections: urlParse.collections },
 							invertCollection: urlParse.invertCollection,
 						},
 					},
 				);
-			return [{ list: metadataGroupsList, url: urlParse }, undefined] as const;
+			return [
+				metadataGroupsList.details.total,
+				{ list: metadataGroupsList, url: urlParse },
+				undefined,
+			] as const;
 		})
 		.with(Action.Search, async () => {
 			const urlParse = zx.parseQuery(request, {
@@ -120,15 +125,35 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 				await serverGqlService.authenticatedRequest(
 					request,
 					MetadataGroupSearchDocument,
-					{ input: { lot, source: urlParse.source, search: { page, query } } },
+					{
+						input: {
+							lot,
+							source: urlParse.source,
+							search: { page: query[pageQueryParam], query: query.query },
+						},
+					},
 				);
 			return [
+				metadataGroupSearch.details.total,
 				undefined,
 				{ search: metadataGroupSearch, url: urlParse, lot },
 			] as const;
 		})
 		.exhaustive();
-	return { action, query, page, list, search, cookieName };
+	const totalPages = await redirectToFirstPageIfOnInvalidPage(
+		request,
+		totalResults,
+		query[pageQueryParam],
+	);
+	return {
+		list,
+		query,
+		action,
+		search,
+		totalPages,
+		cookieName,
+		[pageQueryParam]: query[pageQueryParam],
+	};
 });
 
 export const meta = ({ params }: MetaArgs_SingleFetch<typeof loader>) => {
@@ -137,7 +162,6 @@ export const meta = ({ params }: MetaArgs_SingleFetch<typeof loader>) => {
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
-	const coreDetails = useCoreDetails();
 	const [_, { setP }] = useAppSearchParam(loaderData.cookieName);
 	const navigate = useNavigate();
 	const [
@@ -158,7 +182,11 @@ export default function Page() {
 								$path(
 									"/media/groups/:action",
 									{ action: v },
-									{ query: loaderData.query },
+									{
+										...(loaderData.query.query && {
+											query: loaderData.query.query,
+										}),
+									},
 								),
 							);
 					}}
@@ -176,7 +204,7 @@ export default function Page() {
 				<Group wrap="nowrap">
 					<DebouncedSearchInput
 						placeholder="Search for groups"
-						initialValue={loaderData.query}
+						initialValue={loaderData.query.query}
 						enhancedQueryParams={loaderData.cookieName}
 					/>
 					{loaderData.action === Action.List ? (
@@ -225,27 +253,22 @@ export default function Page() {
 							items found
 						</Box>
 						{loaderData.list.list.details.total > 0 ? (
-							<>
-								<ApplicationGrid>
-									{loaderData.list.list.items.map((gr) => (
-										<MetadataGroupDisplayItem key={gr} metadataGroupId={gr} />
-									))}
-								</ApplicationGrid>
-								<Center>
-									<Pagination
-										size="sm"
-										value={loaderData.page}
-										onChange={(v) => setP("page", v.toString())}
-										total={Math.ceil(
-											loaderData.list.list.details.total /
-												coreDetails.pageLimit,
-										)}
-									/>
-								</Center>
-							</>
+							<ApplicationGrid>
+								{loaderData.list.list.items.map((gr) => (
+									<MetadataGroupDisplayItem key={gr} metadataGroupId={gr} />
+								))}
+							</ApplicationGrid>
 						) : (
 							<Text>No information to display</Text>
 						)}
+						<Center>
+							<Pagination
+								size="sm"
+								total={loaderData.totalPages}
+								value={loaderData[pageQueryParam]}
+								onChange={(v) => setP(pageQueryParam, v.toString())}
+							/>
+						</Center>
 					</>
 				) : null}
 
@@ -258,27 +281,22 @@ export default function Page() {
 							items found
 						</Box>
 						{loaderData.search.search.details.total > 0 ? (
-							<>
-								<ApplicationGrid>
-									{loaderData.search.search.items.map((group) => (
-										<GroupSearchItem item={group} key={group.identifier} />
-									))}
-								</ApplicationGrid>
-								<Center>
-									<Pagination
-										size="sm"
-										value={loaderData.page}
-										onChange={(v) => setP("page", v.toString())}
-										total={Math.ceil(
-											loaderData.search.search.details.total /
-												coreDetails.pageLimit,
-										)}
-									/>
-								</Center>
-							</>
+							<ApplicationGrid>
+								{loaderData.search.search.items.map((group) => (
+									<GroupSearchItem item={group} key={group.identifier} />
+								))}
+							</ApplicationGrid>
 						) : (
 							<Text>No groups found matching your query</Text>
 						)}
+						<Center>
+							<Pagination
+								size="sm"
+								total={loaderData.totalPages}
+								value={loaderData[pageQueryParam]}
+								onChange={(v) => setP(pageQueryParam, v.toString())}
+							/>
+						</Center>
 					</>
 				) : null}
 			</Stack>
diff --git a/apps/frontend/app/routes/_dashboard.media.people.$action.tsx b/apps/frontend/app/routes/_dashboard.media.people.$action.tsx
index 3677470d51..774cc376bc 100644
--- a/apps/frontend/app/routes/_dashboard.media.people.$action.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.people.$action.tsx
@@ -49,10 +49,11 @@ import {
 	FiltersModal,
 } from "~/components/common";
 import { BaseMediaDisplayItem, PersonDisplayItem } from "~/components/media";
-import { commaDelimitedString } from "~/lib/generals";
-import { useAppSearchParam, useCoreDetails } from "~/lib/hooks";
+import { commaDelimitedString, pageQueryParam } from "~/lib/generals";
+import { useAppSearchParam } from "~/lib/hooks";
 import {
 	getEnhancedCookieName,
+	redirectToFirstPageIfOnInvalidPage,
 	redirectUsingEnhancedCookieSearchParams,
 	serverGqlService,
 } from "~/lib/utilities.server";
@@ -85,11 +86,11 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 	const { action } = zx.parseParams(params, { action: z.nativeEnum(Action) });
 	const cookieName = await getEnhancedCookieName(`people.${action}`, request);
 	await redirectUsingEnhancedCookieSearchParams(request, cookieName);
-	const { query, page } = zx.parseQuery(request, {
+	const query = zx.parseQuery(request, {
 		query: z.string().optional(),
-		page: zx.IntAsString.default("1"),
+		[pageQueryParam]: zx.IntAsString.default("1"),
 	});
-	const [peopleList, peopleSearch] = await match(action)
+	const [totalResults, peopleList, peopleSearch] = await match(action)
 		.with(Action.List, async () => {
 			const urlParse = zx.parseQuery(request, {
 				sortBy: z.nativeEnum(PersonSortBy).default(defaultFilters.sortBy),
@@ -102,14 +103,18 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 				PeopleListDocument,
 				{
 					input: {
-						search: { page, query },
-						sort: { by: urlParse.sortBy, order: urlParse.orderBy },
-						filter: { collections: urlParse.collections },
 						invertCollection: urlParse.invertCollection,
+						filter: { collections: urlParse.collections },
+						sort: { by: urlParse.sortBy, order: urlParse.orderBy },
+						search: { page: query[pageQueryParam], query: query.query },
 					},
 				},
 			);
-			return [{ list: peopleList, url: urlParse }, undefined] as const;
+			return [
+				peopleList.details.total,
+				{ list: peopleList, url: urlParse },
+				undefined,
+			] as const;
 		})
 		.with(Action.Search, async () => {
 			const urlParse = zx.parseQuery(request, {
@@ -123,18 +128,35 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 				{
 					input: {
 						source: urlParse.source,
-						search: { page, query },
 						sourceSpecifics: {
 							isAnilistStudio: urlParse.isAnilistStudio,
 							isTmdbCompany: urlParse.isTmdbCompany,
 						},
+						search: { page: query[pageQueryParam], query: query.query },
 					},
 				},
 			);
-			return [undefined, { search: peopleSearch, url: urlParse }] as const;
+			return [
+				peopleSearch.details.total,
+				undefined,
+				{ search: peopleSearch, url: urlParse },
+			] as const;
 		})
 		.exhaustive();
-	return { action, query, page, peopleList, peopleSearch, cookieName };
+	const totalPages = await redirectToFirstPageIfOnInvalidPage(
+		request,
+		totalResults,
+		query[pageQueryParam],
+	);
+	return {
+		query,
+		action,
+		peopleList,
+		totalPages,
+		cookieName,
+		peopleSearch,
+		[pageQueryParam]: query[pageQueryParam],
+	};
 });
 
 export const meta = ({ params }: MetaArgs_SingleFetch<typeof loader>) => {
@@ -143,7 +165,6 @@ export const meta = ({ params }: MetaArgs_SingleFetch<typeof loader>) => {
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
-	const coreDetails = useCoreDetails();
 	const navigate = useNavigate();
 	const [_e, { setP }] = useAppSearchParam(loaderData.cookieName);
 	const [
@@ -164,7 +185,11 @@ export default function Page() {
 								$path(
 									"/media/people/:action",
 									{ action: v },
-									{ query: loaderData.query },
+									{
+										...(loaderData.query.query && {
+											query: loaderData.query.query,
+										}),
+									},
 								),
 							);
 					}}
@@ -182,7 +207,7 @@ export default function Page() {
 				<Group wrap="nowrap">
 					<DebouncedSearchInput
 						placeholder="Search for people"
-						initialValue={loaderData.query}
+						initialValue={loaderData.query.query}
 						enhancedQueryParams={loaderData.cookieName}
 					/>
 					{loaderData.action === Action.List ? (
@@ -248,27 +273,22 @@ export default function Page() {
 							items found
 						</Box>
 						{loaderData.peopleList.list.details.total > 0 ? (
-							<>
-								<ApplicationGrid>
-									{loaderData.peopleList?.list.items.map((person) => (
-										<PersonDisplayItem key={person} personId={person} />
-									))}
-								</ApplicationGrid>
-								<Center>
-									<Pagination
-										size="sm"
-										value={loaderData.page}
-										onChange={(v) => setP("page", v.toString())}
-										total={Math.ceil(
-											loaderData.peopleList.list.details.total /
-												coreDetails.pageLimit,
-										)}
-									/>
-								</Center>
-							</>
+							<ApplicationGrid>
+								{loaderData.peopleList.list.items.map((person) => (
+									<PersonDisplayItem key={person} personId={person} />
+								))}
+							</ApplicationGrid>
 						) : (
 							<Text>No information to display</Text>
 						)}
+						<Center>
+							<Pagination
+								size="sm"
+								total={loaderData.totalPages}
+								value={loaderData[pageQueryParam]}
+								onChange={(v) => setP(pageQueryParam, v.toString())}
+							/>
+						</Center>
 					</>
 				) : null}
 				{loaderData.peopleSearch ? (
@@ -280,27 +300,22 @@ export default function Page() {
 							items found
 						</Box>
 						{loaderData.peopleSearch.search.details.total > 0 ? (
-							<>
-								<ApplicationGrid>
-									{loaderData.peopleSearch.search.items.map((person) => (
-										<PersonSearchItem item={person} key={person.identifier} />
-									))}
-								</ApplicationGrid>
-								<Center>
-									<Pagination
-										size="sm"
-										value={loaderData.page}
-										onChange={(v) => setP("page", v.toString())}
-										total={Math.ceil(
-											loaderData.peopleSearch.search.details.total /
-												coreDetails.pageLimit,
-										)}
-									/>
-								</Center>
-							</>
+							<ApplicationGrid>
+								{loaderData.peopleSearch.search.items.map((person) => (
+									<PersonSearchItem item={person} key={person.identifier} />
+								))}
+							</ApplicationGrid>
 						) : (
 							<Text>No people found matching your query</Text>
 						)}
+						<Center>
+							<Pagination
+								size="sm"
+								total={loaderData.totalPages}
+								value={loaderData[pageQueryParam]}
+								onChange={(v) => setP(pageQueryParam, v.toString())}
+							/>
+						</Center>
 					</>
 				) : null}
 			</Stack>
diff --git a/apps/frontend/app/routes/actions.tsx b/apps/frontend/app/routes/actions.tsx
index 4938fd14a5..a19df9178f 100644
--- a/apps/frontend/app/routes/actions.tsx
+++ b/apps/frontend/app/routes/actions.tsx
@@ -53,8 +53,8 @@ export const loader = async () => redirect($path("/"));
 
 export const action = unstable_defineAction(async ({ request }) => {
 	const formData = await request.clone().formData();
-	const url = new URL(request.url);
-	const intent = url.searchParams.get("intent") as string;
+	const { searchParams } = new URL(request.url);
+	const intent = searchParams.get("intent") as string;
 	invariant(intent);
 	const redirectToForm = formData.get(redirectToQueryParam);
 	let redirectTo = redirectToForm ? redirectToForm.toString() : undefined;


Test patch: