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