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({
>
) : (
<>
-
+
{/* 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 (
(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({