From 85bf3938ba1b4b3a05ef3705342e60d679e3a8b3 Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Thu, 29 Oct 2026 02:34:38 +0700 Subject: [PATCH 1/2] fix(web): compare links with ?cat= open on their category Links across the site and the extension write `/compare?cat=...` while the compare page read only `category`, so they opened on smartphones. The page now reads both; `category` wins when both are set. Co-Authored-By: Claude Opus 5.5 --- apps/web/package.json | 2 +- apps/web/src/app/[locale]/compare/page.tsx | 7 +++++-- apps/web/src/lib/__tests__/compare-params.test.ts | 14 ++++++++++++++ apps/web/src/lib/compare-params.ts | 14 ++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/lib/__tests__/compare-params.test.ts create mode 100644 apps/web/src/lib/compare-params.ts diff --git a/apps/web/package.json b/apps/web/package.json index 787064e1..7aa1380c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -8,7 +8,7 @@ "lint": "eslint . --max-warnings 0", "preview": "next build && next start", "start": "next start", - "test": "pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/server/rate/__tests__/core-rate-architecture.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/server/rate/__tests__/item-model.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/server/rate/__tests__/item-directory.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/app/api/v1/rate/__tests__/write-paths.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/app/api/v1/rate/__tests__/spec-scope.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/app/api/v1/rate/__tests__/item-fields.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/logger.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/review-display.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/concurrency.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/merchant-profile-reads.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/merchant-review-risk.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/item-directory.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/watchlist-sizes.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/item-compare.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/catalog-listing.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/discount-verdict-copy.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/product-detail-copy.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/product-competitors.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/analyzer-copy.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/sold-count.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/content-lang.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/image-url.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/client-messages.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/seo.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/offer-plausibility.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/product-market-stats.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/sitemap-lastmod.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/leaderboard-badge-copy.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/focus-trap.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/display-truth.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/spec-template.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/product-spec-rows.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/sale-state.test.ts", + "test": "pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/server/rate/__tests__/core-rate-architecture.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/server/rate/__tests__/item-model.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/server/rate/__tests__/item-directory.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/app/api/v1/rate/__tests__/write-paths.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/app/api/v1/rate/__tests__/spec-scope.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/app/api/v1/rate/__tests__/item-fields.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/logger.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/review-display.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/concurrency.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/merchant-profile-reads.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/merchant-review-risk.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/item-directory.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/watchlist-sizes.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/item-compare.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/compare-params.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/catalog-listing.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/discount-verdict-copy.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/product-detail-copy.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/product-competitors.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/analyzer-copy.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/sold-count.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/content-lang.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/image-url.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/client-messages.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/seo.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/offer-plausibility.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/product-market-stats.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/sitemap-lastmod.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/leaderboard-badge-copy.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/focus-trap.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/display-truth.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/spec-template.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/product-spec-rows.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/sale-state.test.ts", "typecheck": "tsc --noEmit" }, "browserslist": [ diff --git a/apps/web/src/app/[locale]/compare/page.tsx b/apps/web/src/app/[locale]/compare/page.tsx index 15455bb8..c2bf334a 100644 --- a/apps/web/src/app/[locale]/compare/page.tsx +++ b/apps/web/src/app/[locale]/compare/page.tsx @@ -1,3 +1,4 @@ +import { compareCategoryParam } from '@/lib/compare-params'; import type { Metadata } from 'next'; import { getTranslations, setRequestLocale } from 'next-intl/server'; import { Link } from '@/lib/navigation'; @@ -22,7 +23,7 @@ export const dynamic = 'force-dynamic'; type PageProps = { params: Promise<{ locale: string }>; - searchParams: Promise<{ slugs?: string; category?: string }>; + searchParams: Promise<{ slugs?: string; category?: string; cat?: string }>; }; const VALID_CATEGORIES = new Set([ @@ -121,7 +122,9 @@ export default async function ComparePhonesPage({ setRequestLocale(locale); const lang = normalizeRateLocale(locale); const t = await getTranslations({ locale: lang, namespace: 'compare' }); - const { slugs: slugsParam, category: categoryParam } = await searchParams; + const resolvedSearch = await searchParams; + const slugsParam = resolvedSearch.slugs; + const categoryParam = compareCategoryParam(resolvedSearch); const requestedSlugs = parseSlugsCsv(slugsParam); const [catalogCandidates, requestedProducts] = await Promise.all([ diff --git a/apps/web/src/lib/__tests__/compare-params.test.ts b/apps/web/src/lib/__tests__/compare-params.test.ts new file mode 100644 index 00000000..704f760e --- /dev/null +++ b/apps/web/src/lib/__tests__/compare-params.test.ts @@ -0,0 +1,14 @@ +import assert from 'node:assert/strict'; +import { compareCategoryParam } from '../compare-params'; + +// Compare links written with ?cat= open on their category (DOS/Rate#1303). +assert.equal(compareCategoryParam({ cat: 'earbuds' }), 'earbuds'); +assert.equal(compareCategoryParam({ category: 'laptops' }), 'laptops'); +assert.equal( + compareCategoryParam({ category: 'laptops', cat: 'earbuds' }), + 'laptops', + 'category wins' +); +assert.equal(compareCategoryParam({ cat: ['tablets', 'x'] }), 'tablets'); +assert.equal(compareCategoryParam({}), undefined); +console.log('✅ compare params'); diff --git a/apps/web/src/lib/compare-params.ts b/apps/web/src/lib/compare-params.ts new file mode 100644 index 00000000..bf33adc1 --- /dev/null +++ b/apps/web/src/lib/compare-params.ts @@ -0,0 +1,14 @@ +/** + * The compare page's category query parameter. Links across the site write + * `?cat=` (leaderboard, versus, home, category pages, the extension) while + * the page read only `?category=`, so those links opened on smartphones + * (DOS/Rate#1303). Both are read; `category` wins when both are set. + */ +export function compareCategoryParam(params: { + category?: string | string[]; + cat?: string | string[]; +}): string | undefined { + const first = (v: string | string[] | undefined) => + Array.isArray(v) ? v[0] : v; + return first(params.category) ?? first(params.cat); +} From b70b8c092df054df93901c34b8c3d7d6dc7f3b10 Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Thu, 29 Oct 2026 02:35:56 +0700 Subject: [PATCH 2/2] fix(web): an empty compare category parameter is no value Co-Authored-By: Claude Opus 5.5 --- apps/web/src/app/[locale]/compare/page.tsx | 2 +- apps/web/src/lib/__tests__/compare-params.test.ts | 1 + apps/web/src/lib/compare-params.ts | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/[locale]/compare/page.tsx b/apps/web/src/app/[locale]/compare/page.tsx index c2bf334a..f9fd9113 100644 --- a/apps/web/src/app/[locale]/compare/page.tsx +++ b/apps/web/src/app/[locale]/compare/page.tsx @@ -1,4 +1,3 @@ -import { compareCategoryParam } from '@/lib/compare-params'; import type { Metadata } from 'next'; import { getTranslations, setRequestLocale } from 'next-intl/server'; import { Link } from '@/lib/navigation'; @@ -10,6 +9,7 @@ import { } from '@/server/rate'; import { resolveCompareSlugs } from '@/server/rate/item-compare'; import { getItemSlugsFor } from '@/server/rate/item-directory'; +import { compareCategoryParam } from '@/lib/compare-params'; import { normalizeRateLocale } from '@/lib/rate-i18n'; import { defaultOpenGraph, fitTitle, fitDescription } from '@/lib/seo'; import CompareWorkspace from '@/components/compare/CompareWorkspace'; diff --git a/apps/web/src/lib/__tests__/compare-params.test.ts b/apps/web/src/lib/__tests__/compare-params.test.ts index 704f760e..6315f23b 100644 --- a/apps/web/src/lib/__tests__/compare-params.test.ts +++ b/apps/web/src/lib/__tests__/compare-params.test.ts @@ -11,4 +11,5 @@ assert.equal( ); assert.equal(compareCategoryParam({ cat: ['tablets', 'x'] }), 'tablets'); assert.equal(compareCategoryParam({}), undefined); +assert.equal(compareCategoryParam({ category: '', cat: 'earbuds' }), 'earbuds'); console.log('✅ compare params'); diff --git a/apps/web/src/lib/compare-params.ts b/apps/web/src/lib/compare-params.ts index bf33adc1..66cb67d8 100644 --- a/apps/web/src/lib/compare-params.ts +++ b/apps/web/src/lib/compare-params.ts @@ -10,5 +10,6 @@ export function compareCategoryParam(params: { }): string | undefined { const first = (v: string | string[] | undefined) => Array.isArray(v) ? v[0] : v; - return first(params.category) ?? first(params.cat); + // An empty value is no value, as in the API routes. + return first(params.category) || first(params.cat) || undefined; }