diff --git a/src/app/compare/[slug]/loading.tsx b/src/app/compare/[slug]/loading.tsx deleted file mode 100644 index 2597da75..00000000 --- a/src/app/compare/[slug]/loading.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Loading UI rendered by Next.js during navigation to /compare/[slug]. - * Picked up automatically when the route's async render is in flight, - * which is the visible window where ad-hoc (non-curated) pairs pay the - * full cold start cost: every loadBenchmark for every shared bench - * fans out chain + region variant fetches. Without this file the user - * sees a frozen current page while the browser waits on the route - * payload; with it the visitor gets instant feedback that the compare - * page is building. - */ -export default function ComparePairLoading() { - return ( -
-
- -
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-

- Loading live measurements -

-
- {Array.from({ length: 3 }).map((_, i) => ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))} -
-

- First hit on a brand new pair can take a few seconds while the - per chain and per region variants fan out. Subsequent visits - and other users land on the cached render. -

-
-
- ); -} diff --git a/src/app/compare/[slug]/page.tsx b/src/app/compare/[slug]/page.tsx index dcf68a45..c4bff5a8 100644 --- a/src/app/compare/[slug]/page.tsx +++ b/src/app/compare/[slug]/page.tsx @@ -22,6 +22,7 @@ import { buildSharedBenches, canonicalisationTarget, fmtTs, + hasSharedBenches, latestIso, parseAdHocSlug, type BreakdownRow, @@ -120,6 +121,14 @@ export async function generateMetadata({ if (!pair) notFound(); const { a, b } = await loadPairProviders(pair); if (!a || !b) notFound(); + // Final SSR gate: an ad-hoc pair can have both providers resolved yet + // share zero benches (e.g. an RPC provider vs an oracle). Without + // this notFound() the page body's `shared.length === 0` check fires + // after loading.tsx has already streamed the shell, so the response + // ships HTTP 200 + skeleton + a noindex meta from not-found.tsx — + // exactly what crawlers indexed before this fix. Cheap: only the + // appearance intersection, no Prom fan out. + if (!hasSharedBenches(pair, a, b)) notFound(); const title = `${a.name} vs ${b.name}: live OpenChainBench benchmark data`; const description = capDescription( diff --git a/src/lib/compare-compute.ts b/src/lib/compare-compute.ts index a4130be8..736fda69 100644 --- a/src/lib/compare-compute.ts +++ b/src/lib/compare-compute.ts @@ -248,6 +248,38 @@ export async function loadChainRegionMatrix( return entries; } +/** Lightweight precheck: does this pair have at least one shared bench + * after applying the whitelist + exclude rules? Pure set arithmetic on + * the already-loaded provider appearances. No Prom calls, no KV + * lookup, no fan out. + * + * Used by `generateMetadata` so the route can `notFound()` BEFORE + * Next.js streams the loading.tsx fallback. Without this, an + * unresolvable ad-hoc pair (two real providers that share zero benches, + * e.g. an RPC provider vs an oracle) ships HTTP 200 + the loading + * skeleton + `Page not found` because the + * `shared.length === 0` check inside the page body fires after the + * Suspense boundary has already streamed the shell. The result was + * Google indexing the skeleton with `robots: noindex` for the entire + * /compare ad-hoc surface. + * + * Mirrors the candidate-slug computation inside `buildSharedBenches` + * so the two stay in lockstep. */ +export function hasSharedBenches( + pair: ComparePair, + aAppearances: Awaited>, + bAppearances: Awaited>, +): boolean { + if (!aAppearances || !bAppearances) return false; + const aSlugs = new Set(aAppearances.appearances.map((x) => x.benchmark.slug)); + const bSlugs = new Set(bAppearances.appearances.map((x) => x.benchmark.slug)); + const candidateSlugs = pair.benchmarks + ? pair.benchmarks.filter((s) => aSlugs.has(s) && bSlugs.has(s)) + : Array.from(aSlugs).filter((s) => bSlugs.has(s)); + const excluded = new Set(pair.excludeBenchmarks ?? []); + return candidateSlugs.some((s) => !excluded.has(s)); +} + /** Resolves the intersection of two providers' bench appearances, then * enriches each shared bench with aggregate + per chain + per region * breakdowns. Honors the pair's `benchmarks` whitelist (when set) and