From 44e16220025078f3946bde48697d0a4c27779ee1 Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sat, 11 Jul 2026 22:27:04 +0200 Subject: [PATCH 1/5] compare: live-data emission floor, noindex thin ad hoc pairs, brand casing (#1099) * compare SEO: live-data pair gate, noindex thin pairs, brand name casing * compare: curated pairs exempt from thin noindex gate --------- Co-authored-by: Florent Tapponnier --- src/app/compare/[slug]/page.tsx | 37 ++++++++++++++++++++ src/app/compare/page.tsx | 25 +++++++++++--- src/app/sitemap.ts | 24 +++++++------ src/lib/compare-pairing.ts | 3 +- src/lib/compare/adhoc-pairs.ts | 46 ++++++++++++++++++------- src/lib/providers.ts | 60 +++++++++++++++++++++++++++++++-- 6 files changed, 162 insertions(+), 33 deletions(-) diff --git a/src/app/compare/[slug]/page.tsx b/src/app/compare/[slug]/page.tsx index fccc0a52..a74c8b1d 100644 --- a/src/app/compare/[slug]/page.tsx +++ b/src/app/compare/[slug]/page.tsx @@ -198,6 +198,40 @@ export async function generateMetadata({ const sharedCount = sharedSlugsForMeta.filter((s) => !excluded.has(s)).length; const benchWord = sharedCount === 1 ? "benchmark" : "benchmarks"; + // Thin-content gate (SEO audit 2026-07-08): a pair whose shared + // benches carry live data for both providers on fewer than 2 of them + // renders either "awaiting live measurements" or a single card. + // Those pages stay reachable (internal links + stale index entries + // must not 404) but are marked noindex so direct hits stop counting + // against the domain. Mirrors the >= 2 live-shared emission floor in + // src/lib/compare/adhoc-pairs.ts so the sitemap never advertises a + // noindexed URL. Live rule matches liveResults(): not "unavailable" + // and p50 > 0, read off the already-loaded appearances. + const aLive = new Set( + a.appearances + .filter( + (x) => x.result.availability !== "unavailable" && x.result.ms.p50 > 0, + ) + .map((x) => x.benchmark.slug), + ); + const bLive = new Set( + b.appearances + .filter( + (x) => x.result.availability !== "unavailable" && x.result.ms.p50 > 0, + ) + .map((x) => x.benchmark.slug), + ); + const liveSharedCount = sharedSlugsForMeta.filter( + (s) => !excluded.has(s) && aLive.has(s) && bLive.has(s), + ).length; + // Curated pairs (explicit benchmarks list in COMPARE_PAIRS) are + // hand-picked head-term targets like usdc-vs-usdt and carry editorial + // framing beyond the ledger, so they stay indexable even with a + // single live shared bench. The gate only applies to combinatorial + // ad hoc pairs. + const isCurated = !!pair.benchmarks; + const thin = !isCurated && liveSharedCount < 2; + // Meta description: unique per pair via the shared-count + provider // names + date. Kills the identical duplicate-content signal that had // Bing indexing 2 of 4938 compare pages. Also cites "as of DATE" for @@ -211,6 +245,9 @@ export async function generateMetadata({ return { title, description, + // follow stays on so PageRank keeps flowing through the body links + // (both provider pages, parent benches) even while deindexed. + ...(thin ? { robots: { index: false, follow: true } } : {}), alternates: { canonical: url }, openGraph: { title, diff --git a/src/app/compare/page.tsx b/src/app/compare/page.tsx index 7b955085..80f3c5c4 100644 --- a/src/app/compare/page.tsx +++ b/src/app/compare/page.tsx @@ -49,11 +49,20 @@ export default async function ComparePage() { // enumeration as sitemap.ts (shared lib), minus the curated pairs // already rendered above. const curatedSlugs = new Set(pairs.map((p) => p.slug)); - const morePairs = adHocPairs(await getProviders()) + const profiles = await getProviders(); + // Prefer the profile display name (carries the bench spec's brand + // casing: dRPC, 1RPC, USDC) over the canonicalize() title-case + // fallback, which rendered "Drpc vs Tenderly" style labels here. + const nameBySlug = new Map(profiles.map((p) => [p.slug, p.name])); + const displayName = (slug: string): string => { + const canon = canonicalize(slug); + return nameBySlug.get(canon.slug) ?? canon.name; + }; + const morePairs = adHocPairs(profiles) .filter((p) => !curatedSlugs.has(p.slug)) .map((p) => ({ ...p, - label: `${canonicalize(p.a).name} vs ${canonicalize(p.b).name}`, + label: `${displayName(p.a)} vs ${displayName(p.b)}`, })) .sort((a, b) => a.label.localeCompare(b.label)); @@ -64,7 +73,7 @@ export default async function ComparePage() { itemListElement: pairs.map((pair, i) => ({ "@type": "ListItem", position: i + 1, - name: `${canonicalize(pair.providerA).name} vs ${canonicalize(pair.providerB).name}`, + name: `${displayName(pair.providerA)} vs ${displayName(pair.providerB)}`, url: `${SITE.url}/compare/${pair.slug}`, })), }; @@ -101,8 +110,14 @@ export default async function ComparePage() {