From 9ebed7b935026405bd5403f97e18cacedce43253 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Tue, 8 Sep 2026 11:52:35 +0200 Subject: [PATCH] fix: single-origin bench payload guard + sitemap lists only servable product URLs (dev twin) Same two changes as the main hotfix. On dev, keyed-rpc-robinhood declares its region tabs so the payload guard is a no-op there; the sitemap filter is identical. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CpArutAtXuBb1BVNUDXoYA --- src/app/benchmarks/[slug]/page.tsx | 41 +++++++++++++++++++++++++++++- src/lib/sitemap-builder.ts | 15 +++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/app/benchmarks/[slug]/page.tsx b/src/app/benchmarks/[slug]/page.tsx index 1c6037470..bccc93431 100644 --- a/src/app/benchmarks/[slug]/page.tsx +++ b/src/app/benchmarks/[slug]/page.tsx @@ -271,7 +271,46 @@ export default async function BenchmarkPage({ const variants: Record = { [variantKey(null, null, null)]: aggregate, }; - const benchmark = aggregate; + // Single-origin surface guard. When this build pins one region + // (aggregate_filters.region) and declares no region dimension, the page + // must not ship the other regions' data in its payload either: the blob + // carries every region the worker computed, and whatever is passed to a + // client component lands in the RSC payload, readable in view-source even + // though nothing renders it. Keep the pinned region's cells, drop the + // rest. Benches that declare region tabs are untouched. + const pinnedRegion = aggregate.aggregateFilters?.region; + const singleOrigin = regionOptions.length === 0 && typeof pinnedRegion === "string"; + const samePinned = (r: string) => canonRegion(r) === canonRegion(pinnedRegion ?? ""); + const benchmark = !singleOrigin + ? aggregate + : { + ...aggregate, + extras: { + ...aggregate.extras, + regions: Object.fromEntries( + Object.entries(aggregate.extras.regions ?? {}).map(([slug, pts]) => [ + slug, + (pts ?? []).filter((pt) => samePinned(pt.region)), + ]), + ), + seriesByRegion24h: aggregate.extras.seriesByRegion24h + ? Object.fromEntries( + Object.entries(aggregate.extras.seriesByRegion24h).map(([slug, byRegion]) => [ + slug, + Object.fromEntries(Object.entries(byRegion).filter(([r]) => samePinned(r))), + ]), + ) + : undefined, + }, + cellRanks: aggregate.cellRanks + ? Object.fromEntries( + Object.entries(aggregate.cellRanks).filter(([key]) => { + const r = key.split("|").pop() ?? ""; + return r === "" || r === "all" || samePinned(r); + }), + ) + : undefined, + }; const isDraft = benchmark.status === "draft"; const isAwaiting = isDraft && benchmark.editorialStatus === "live"; diff --git a/src/lib/sitemap-builder.ts b/src/lib/sitemap-builder.ts index 9be2e3f3c..c75c298d7 100644 --- a/src/lib/sitemap-builder.ts +++ b/src/lib/sitemap-builder.ts @@ -6,6 +6,8 @@ import { COMPARE_PAIRS } from "@/data/compare-pairs"; import { REMOVED_BENCH_SLUGS } from "@/middleware"; import { REMOVED_PRODUCT_SLUGS } from "@/lib/removed-benches"; import { isHlBuilderSlug } from "@/lib/hl-builder-stats"; +import { getSpecs } from "@/lib/spec"; +import { PROVIDER_REGISTRY } from "@/data/provider-registry"; import { PERP_PRODUCT_PILL_SLUGS } from "@/lib/perp-venue-context"; import { loadAllAlternatives } from "@/lib/alternatives"; import { loadAllAnswers } from "@/lib/answers"; @@ -277,9 +279,22 @@ async function buildFullSitemap(): Promise { // Prom) so it's safe async — no OOM risk (unlike the old getProvider fan-out). // It also catches dormant HL frontends missing from the Prom cohort that // the worker couldn't filter without the spec provider list. + // Only list product pages this build can actually serve. The blob is + // produced by the worker from its own checkout, so it can name providers + // that a spec on THIS branch does not declare yet; /products/ then + // 404s and the deploy's sitemap smoke blocks the release (2026-09-08: + // /products/serialized, declared on dev, listed in prod's sitemap). + const declaredProviderSlugs = new Set(); + for (const spec of await getSpecs()) { + for (const p of spec.providers ?? []) declaredProviderSlugs.add(p.slug); + } + for (const entry of Object.values(PROVIDER_REGISTRY)) { + if (entry.parent) declaredProviderSlugs.add(entry.parent); + } const validatedSlugs = ( await Promise.all( providerSlugs.map(async (slug) => { + if (!declaredProviderSlugs.has(slug)) return null; if (CHAIN_BY_SLUG.has(slug)) return null; if (hlBuilderSlugSet.has(slug)) return null; if (await isHlBuilderSlug(slug)) return null;