From 7e3b37a2b7b0085a1cf89eb9cf690d25f57a7ed6 Mon Sep 17 00:00:00 2001 From: Flotapponnier Date: Sat, 19 Sep 2026 14:33:34 +0200 Subject: [PATCH] seo: hubs link only indexable RPC pages; ledger and region labels are H2; rpc-capabilities chain redirects complete; 60 s copy; marketing-adjective lint Audit (ocb-seo-geo-audit, scope rpc) majors 3, 5, 7 and minors 3, 8. - isThinRpcBench() is the one thin gate (category RPCs, fewer than 3 results): the bench page's noindex uses it, the /benchmarks index drops those benches from the grid and the ItemList, /rpc restricts its chain list, ItemList and quick links to the benches the worker publishes in the sitemap, and the chains leaderboard shows a thin chain as a plain row "not yet ranked" instead of a link. /rpc linked 153 chain pages, 36 of them noindex; it links 117 now. - "Product ledger" and "By region" labels are

(same classes), so the outline reads Product ledger / By region / Methodology / Public endpoints / Head to head / Frequently asked. - /benchmarks/rpc-capabilities/ 308s cover gnosis, sonic, solana and polkadot too (they 404ed). - /rpc said "every 15 seconds" three times; the probe is 60 s. - seoText refuses blisteringly, blazing, best-in-class, world-class, revolutionary, lightning-fast, ultra-fast, cutting-edge, game-changing ("trusted" and "powerful" stay allowed: technical usage in two specs). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HJgbZCqjR4nvCfcJSzofbw --- next.config.ts | 4 ++++ src/app/benchmarks/[slug]/page.tsx | 3 ++- src/app/benchmarks/page.tsx | 5 ++++- src/app/rpc/page.tsx | 15 +++++++++---- src/components/benchmark-body.tsx | 6 +++--- src/components/rpc-chains-leaderboard.tsx | 26 +++++++++++++++++++---- src/lib/provider-filters.ts | 12 +++++++++++ src/lib/spec-schema.ts | 10 ++++++++- 8 files changed, 67 insertions(+), 14 deletions(-) diff --git a/next.config.ts b/next.config.ts index 458105e14..f21e589b9 100644 --- a/next.config.ts +++ b/next.config.ts @@ -195,6 +195,10 @@ const nextConfig: NextConfig = { "linea", "scroll", "mantle", + "gnosis", + "sonic", + "solana", + "polkadot", ]; const rpcClusterRedirects = RPC_CLUSTER_CHAINS.map((chain) => ({ source: `/benchmarks/rpc-capabilities/${chain}`, diff --git a/src/app/benchmarks/[slug]/page.tsx b/src/app/benchmarks/[slug]/page.tsx index 5c861d05a..4b44bda4b 100644 --- a/src/app/benchmarks/[slug]/page.tsx +++ b/src/app/benchmarks/[slug]/page.tsx @@ -13,6 +13,7 @@ import { TerminalFillAudit } from "@/components/terminal-fill-audit"; import { Breadcrumb } from "@/components/breadcrumb"; import { ChainHeadingsSummary } from "@/components/chain-headings-summary"; import { CompareThisBench } from "@/components/compare-this-bench"; +import { isThinRpcBench } from "@/lib/provider-filters"; import { PublicEndpointsSection } from "@/components/public-endpoints-section"; import { CitationBar } from "@/components/citation-bar"; import { LiveIndicator } from "@/components/live-indicator"; @@ -124,7 +125,7 @@ export async function generateMetadata({ // provider leaderboard has no comparative value and no search demand. // noindex/follow keeps crawl equity flowing without letting an // empty-looking table rank for "fastest X rpc". - const metaThinRpc = b.category === "RPCs" && (b.results?.length ?? 0) < 3; + const metaThinRpc = isThinRpcBench(b); const metaTitle = b.seoTitle ?? b.title; // Description precedence (most-to-least specific): // 1. `seo_description` from the YAML - hand-crafted snippet with the diff --git a/src/app/benchmarks/page.tsx b/src/app/benchmarks/page.tsx index 1a76bd333..d2762c6c7 100644 --- a/src/app/benchmarks/page.tsx +++ b/src/app/benchmarks/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { getBenchmarksSafe, toBenchmarkCardData } from "@/data/benchmarks"; +import { isThinRpcBench } from "@/lib/provider-filters"; import { BenchmarkGrid } from "@/components/benchmark-grid"; import { safeJsonLd } from "@/lib/jsonld"; @@ -28,7 +29,9 @@ export const metadata: Metadata = { }; export default async function BenchmarksPage() { - const benchmarks = await getBenchmarksSafe(); + // Thin chain RPC benches (< 3 providers) are noindex on their own page; + // the index does not link them (see isThinRpcBench). + const benchmarks = (await getBenchmarksSafe()).filter((b) => !isThinRpcBench(b)); // ItemList + BreadcrumbList JSON-LD so search engines and LLMs see the // page as a structured registry (the data is already in the DOM but diff --git a/src/app/rpc/page.tsx b/src/app/rpc/page.tsx index afdaad4c5..7c696dc6b 100644 --- a/src/app/rpc/page.tsx +++ b/src/app/rpc/page.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; import { fetchRpcHub, NON_CHAIN_RPC_SLUGS } from "@/lib/rpc-hub-stats"; +import { loadSitemapBlob } from "@/lib/sitemap-blob"; import { getSpecs } from "@/lib/spec"; import { RpcHubTabs } from "@/components/rpc-hub-tabs"; import { pageMetadata } from "@/lib/page-metadata"; @@ -31,11 +32,17 @@ export const metadata: import("next").Metadata = pageMetadata({ export const revalidate = 3600; export default async function RpcHubPage() { - const [snapshot, specs] = await Promise.all([fetchRpcHub(), getSpecs()]); + const [snapshot, specs, sitemapBlob] = await Promise.all([fetchRpcHub(), getSpecs(), loadSitemapBlob()]); // Spec-derived chain list: stable across snapshot outages, so the // JSON-LD ItemList and the empty state never churn with data blips. + // Restricted to the benches the worker publishes in the sitemap: a chain + // bench under the thin gate is noindex on its own page and must not be + // linked from here (36 such links on 2026-09-19). Without the blob, no + // restriction rather than an empty hub. + const indexable = sitemapBlob ? new Set(sitemapBlob.benches.map((b) => b.slug)) : null; const rpcSpecs = specs .filter((s) => s.slug.endsWith("-rpc") && !NON_CHAIN_RPC_SLUGS.has(s.slug)) + .filter((s) => !indexable || indexable.has(s.slug)) .sort((a, b) => a.slug.localeCompare(b.slug)); // Keyed / API-key RPC benches (e.g. keyed-rpc-robinhood). These do NOT // belong in the no-key chain matrix above (different tier, and the @@ -61,7 +68,7 @@ export default async function RpcHubPage() { "@type": "ItemList", name: "Per-chain free RPC benchmarks by OpenChainBench", description: - "Live per-chain benchmarks of free, no-key public RPC endpoints: latency, reliability and archive depth measured every 15 seconds from 3 regions.", + "Live per-chain benchmarks of free, no-key public RPC endpoints: latency, reliability and archive depth measured every 60 seconds from 3 regions.", numberOfItems: rpcSpecs.length, itemListElement: rpcSpecs.map((s, i) => ({ "@type": "ListItem", @@ -119,7 +126,7 @@ export default async function RpcHubPage() { Every free, no-key public RPC endpoint, measured per chain with the same probe: one identical{" "} eth_getBlockByNumber("latest", false) call with a - rotating request id (defeats CDN body-keyed caches) every 15 + rotating request id (defeats CDN body-keyed caches) every 60 seconds from 3 regions (N. Virginia, Amsterdam, Singapore). The matrix below folds the per-chain leaderboards into one view: fastest provider per chain, fastest per region, and which gateway covers your whole multichain stack. Headline @@ -277,7 +284,7 @@ export default async function RpcHubPage() {

Each chain row aggregates that chain's dedicated bench: an identical JSON-RPC POST (eth_getBlockByNumber("latest", false) with - rotating request id, or the chain's equivalent head call) sent every 15 seconds to + rotating request id, or the chain's equivalent head call) sent every 60 seconds to every free, no-key public endpoint from us-east, eu-west and Singapore. Headline figures are the 50th percentile of client-side round-trip latency over the trailing 24 hours, diff --git a/src/components/benchmark-body.tsx b/src/components/benchmark-body.tsx index 96db79a7a..7af788124 100644 --- a/src/components/benchmark-body.tsx +++ b/src/components/benchmark-body.tsx @@ -999,7 +999,7 @@ export function BenchmarkBody({ ) : ( <> -

+

{viewBenchmark.unit === "count" ? "Product ledger" : activePanel @@ -1007,7 +1007,7 @@ export function BenchmarkBody({ : viewBenchmark.ledgerColumns?.length ? `Product ledger · sorted by ${viewBenchmark.ledgerColumns[0].label}` : "Product ledger · sorted by p50"} -

+

{/* Visible freshness stamp next to the numbers. Answer engines quote data far more readily when the page says @@ -1027,7 +1027,7 @@ export function BenchmarkBody({ {viewBenchmark.unit !== "count" && Object.keys(benchmark.extras.regions).length > 0 && (
-

By region

+

By region

)} diff --git a/src/components/rpc-chains-leaderboard.tsx b/src/components/rpc-chains-leaderboard.tsx index b1a42dd65..677998294 100644 --- a/src/components/rpc-chains-leaderboard.tsx +++ b/src/components/rpc-chains-leaderboard.tsx @@ -2,6 +2,7 @@ import { useMemo, useState } from "react"; import Link from "next/link"; +import { THIN_RPC_MIN_RESULTS } from "@/lib/provider-filters"; import { useRouter } from "next/navigation"; import { ChevronRight } from "lucide-react"; import { ProviderLogo } from "@/components/provider-logo"; @@ -161,17 +162,23 @@ export function RpcChainsLeaderboard({ rows }: { rows: RpcHubChain[] }) { - {filtered.map((r, i) => ( + {filtered.map((r, i) => { + // Under the thin gate (< 3 providers) the chain page is noindex: + // show the row, do not link it (the hub was the main source of + // noindex crawl on 2026-09-19). + const linkable = r.providerCount >= THIN_RPC_MIN_RESULTS; + return ( router.push(`/benchmarks/${r.slug}`)} - className="border-t border-ink/5 hover:bg-paper-soft/40 transition-colors cursor-pointer" + onClick={linkable ? () => router.push(`/benchmarks/${r.slug}`) : undefined} + className={`border-t border-ink/5 transition-colors ${linkable ? "hover:bg-paper-soft/40 cursor-pointer" : ""}`} > {i + 1}
+ {linkable ? ( + ) : ( + + + {r.name} + not yet ranked + + )} - ))} + ); + })} {filtered.length === 0 && ( (r.successRate ?? 100) >= MIN_DISPLAY_SUCCESS_PCT, ); } + +/** + * The RPC thin gate, in one place. A chain RPC bench with fewer than three + * declared results is noindex (src/app/benchmarks/[slug]/page.tsx) and the + * worker keeps it out of the sitemap; hubs and the benchmarks index must not + * link it either (Search Console 2026-09-19: /rpc and /benchmarks linked 36 + * such pages, crawl spent on noindex dead ends). + */ +export const THIN_RPC_MIN_RESULTS = 3; +export function isThinRpcBench(b: { category: string; results?: unknown[] | null }): boolean { + return b.category === "RPCs" && (b.results?.length ?? 0) < THIN_RPC_MIN_RESULTS; +} diff --git a/src/lib/spec-schema.ts b/src/lib/spec-schema.ts index 94bbc37ca..f13a733d1 100644 --- a/src/lib/spec-schema.ts +++ b/src/lib/spec-schema.ts @@ -212,12 +212,20 @@ const noAiDashes = (s: string) => !s.includes("—") && !s.includes("–"); const noAiDashesMsg = "Avoid em or en dashes; use a comma, semicolon, or period instead"; +// Marketing adjectives the editorial rules forbid in copy; a measured claim +// ("fastest ... measured") is fine, these words never are. +// "trusted" and "powerful" are left out on purpose: both occur as plain +// technical verbs and adjectives ("timestamps are never trusted"). +const MARKETING = /\b(blisteringly|blazing(ly)?|best-in-class|world-class|revolutionary|lightning-fast|ultra-fast|cutting-edge|game-changing)\b/i; +const noMarketing = (s: string) => !MARKETING.test(s); +const noMarketingMsg = "marketing adjective (blisteringly, blazing, best-in-class, world-class, ...): state the number instead"; const seoText = (min: number, max: number) => z .string() .min(min) .max(max) - .refine(noAiDashes, noAiDashesMsg); + .refine(noAiDashes, noAiDashesMsg) + .refine(noMarketing, noMarketingMsg); export const SpecSchema = z .object({