Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 0 additions & 84 deletions src/app/compare/[slug]/loading.tsx

This file was deleted.

9 changes: 9 additions & 0 deletions src/app/compare/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
buildSharedBenches,
canonicalisationTarget,
fmtTs,
hasSharedBenches,
latestIso,
parseAdHocSlug,
type BreakdownRow,
Expand Down Expand Up @@ -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(
Expand Down
32 changes: 32 additions & 0 deletions src/lib/compare-compute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 + `<title>Page not found</title>` 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<ReturnType<typeof getProvider>>,
bAppearances: Awaited<ReturnType<typeof getProvider>>,
): 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
Expand Down
Loading