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
4 changes: 4 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand Down
3 changes: 2 additions & 1 deletion src/app/benchmarks/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion src/app/benchmarks/page.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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
Expand Down
15 changes: 11 additions & 4 deletions src/app/rpc/page.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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
Expand All @@ -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",
Expand Down Expand Up @@ -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{" "}
<code>eth_getBlockByNumber(&quot;latest&quot;, false)</code> 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
Expand Down Expand Up @@ -277,7 +284,7 @@ export default async function RpcHubPage() {
<p>
Each chain row aggregates that chain&apos;s dedicated bench: an
identical JSON-RPC POST (<code>eth_getBlockByNumber(&quot;latest&quot;, false)</code> with
rotating request id, or the chain&apos;s equivalent head call) sent every 15 seconds to
rotating request id, or the chain&apos;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,
Expand Down
6 changes: 3 additions & 3 deletions src/components/benchmark-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -999,15 +999,15 @@ export function BenchmarkBody({
</>
) : (
<>
<p className="label-mono text-ink-faint mb-4">
<h2 className="label-mono text-ink-faint mb-4">
{viewBenchmark.unit === "count"
? "Product ledger"
: activePanel
? `Product ledger · sorted by ${activePanel.label}`
: viewBenchmark.ledgerColumns?.length
? `Product ledger · sorted by ${viewBenchmark.ledgerColumns[0].label}`
: "Product ledger · sorted by p50"}
</p>
</h2>
<LedgerTable benchmark={viewBenchmark} activePanel={activePanel} topN={topN} />
{/* Visible freshness stamp next to the numbers. Answer
engines quote data far more readily when the page says
Expand All @@ -1027,7 +1027,7 @@ export function BenchmarkBody({
{viewBenchmark.unit !== "count" &&
Object.keys(benchmark.extras.regions).length > 0 && (
<div className={"mt-8 card-soft rounded-xl p-4 sm:p-6 lg:p-8" + pendingCls}>
<p className="label-mono text-ink-faint mb-4">By region</p>
<h2 className="label-mono text-ink-faint mb-4">By region</h2>
<RegionGrid benchmark={viewBenchmark} />
</div>
)}
Expand Down
26 changes: 22 additions & 4 deletions src/components/rpc-chains-leaderboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -161,17 +162,23 @@ export function RpcChainsLeaderboard({ rows }: { rows: RpcHubChain[] }) {
</tr>
</thead>
<tbody>
{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 (
<tr
key={r.slug}
onClick={() => 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" : ""}`}
>
<Td muted mono>
{i + 1}
</Td>
<Td>
<div className="flex items-center gap-2 min-w-0">
{linkable ? (
<Link
href={`/benchmarks/${r.slug}`}
className="flex items-center gap-2 min-w-0 group"
Expand All @@ -186,6 +193,16 @@ export function RpcChainsLeaderboard({ rows }: { rows: RpcHubChain[] }) {
className="text-ink-faint shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
/>
</Link>
) : (
<span
className="flex items-center gap-2 min-w-0"
title="Measured, not yet ranked: fewer than 3 no-key providers"
>
<ProviderLogo slug={r.chain} name={r.name} size={18} />
<span className="font-medium text-ink truncate">{r.name}</span>
<span className="text-[10px] text-ink-faint shrink-0">not yet ranked</span>
</span>
)}
<Link
href={`/chains/${r.chain}`}
className="text-[10px] text-ink-faint hover:text-ink underline underline-offset-2 shrink-0"
Expand Down Expand Up @@ -308,7 +325,8 @@ export function RpcChainsLeaderboard({ rows }: { rows: RpcHubChain[] }) {
)}
</Td>
</tr>
))}
);
})}
{filtered.length === 0 && (
<tr>
<td
Expand Down
12 changes: 12 additions & 0 deletions src/lib/provider-filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,15 @@ export function displayResults(results: ProviderResult[]): ProviderResult[] {
(r) => (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;
}
10 changes: 9 additions & 1 deletion src/lib/spec-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading