diff --git a/src/app/benchmarks/[slug]/page.tsx b/src/app/benchmarks/[slug]/page.tsx index dd95803d8..0e14c7f7c 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 d2d115c0d..3e677950d 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"; @@ -275,9 +277,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;