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
32 changes: 18 additions & 14 deletions src/app/api/citable/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@ import { NextResponse } from "next/server";
import { getBenchmarks } from "@/data/benchmarks";
import { SITE } from "@/data/site";
import { AllBenchmarksDraftError } from "@/lib/spec";
import {
fieldValue,
headlineSentence,
isInsufficient,
leader,
} from "@/lib/citation";
import { citeBundle, fieldValue, leader, headlineSentence } from "@/lib/citation";
import { clientKey, rateLimit, tooManyRequests } from "@/lib/rate-limit";

export const runtime = "nodejs";
Expand Down Expand Up @@ -52,28 +47,37 @@ export async function GET(req: Request) {
throw err;
}
const data = benches.map((b) => {
const insufficient = isInsufficient(b);
const top = insufficient ? null : leader(b);
const status: "live" | "draft" | "insufficient" = insufficient
? "insufficient"
: b.status;
const top = leader(b);
// Insufficient aggregate: explicitly null the value + leader so
// downstream LLM agents and journalists do not quote a number drawn
// from an undersized field. The headline sentence is rewritten to
// "insufficient data" by headlineSentence above.
const insufficient = b.dataConfidence === "insufficient";
return {
slug: b.slug,
title: b.title,
category: b.category,
metric: b.metric,
unit: b.unit,
status,
status: b.status,
value: insufficient ? null : fieldValue(b),
leader: top ? { name: top.name, slug: top.slug, value: top.value } : null,
sampleSize: insufficient ? 0 : b.sampleSize,
leader:
insufficient
? null
: top
? { name: top.name, slug: top.slug, value: top.value }
: null,
sampleSize: b.sampleSize,
expectedN: b.expectedN,
dataConfidence: b.dataConfidence,
asOf: b.lastRunAt,
headline: headlineSentence(b),
url: `${SITE.url}/benchmarks/${b.slug}`,
api: `${SITE.url}/api/stat/${b.slug}`,
ogImage: `${SITE.url}/api/og/${b.slug}`,
source: b.source,
license: "CC-BY-4.0",
cite: citeBundle(b, SITE.url),
};
});

Expand Down
95 changes: 32 additions & 63 deletions src/app/api/stat/[slug]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { getBenchmark } from "@/data/benchmarks";
import { SITE } from "@/data/site";
import {
citationQuote,
citeBundle,
fieldValue,
headlineSentence,
isInsufficient,
leader,
sparklineFor,
} from "@/lib/citation";
Expand All @@ -20,16 +20,6 @@ export const revalidate = 60;
* Single benchmark as a citable atomic unit. Designed to fit into one
* agent tool call: ranked providers, sparkline, methodology link,
* pre-formatted attribution string, and stable citation URL.
*
* Status field semantics:
* "live" - usable measurement, leader / value populated.
* "draft" - spec author has not published (editorialStatus draft).
* "insufficient" - editorially live but the harness has no usable
* sample yet (every provider p50 = 0, or runtime
* status flipped to draft mid-cycle). value, leader
* and rankings p50 are nulled so a consumer cannot
* accidentally cite a fabricated winner. The shape
* of the response is preserved.
*/
export async function GET(
req: Request,
Expand All @@ -53,74 +43,53 @@ export async function GET(
);
}

const insufficient = isInsufficient(b);
const top = insufficient ? null : leader(b);
const value = insufficient ? null : fieldValue(b);
// Status surfaced to consumers: "insufficient" wins over the raw
// runtime "live" flag when the predicate fires, so /api/stat stops
// claiming live data for a bench whose harness has nothing to show.
const status: "live" | "draft" | "insufficient" = insufficient
? "insufficient"
: b.status;

// Rankings: when insufficient we still return one entry per provider
// (shape preserved for any consumer that diff-tracks the provider set)
// but every p50 is nulled to drive home that no comparison is possible.
const rankings = insufficient
? b.results.map((r) => ({
name: r.name,
slug: r.slug,
type: r.type ?? null,
layer: r.layer ?? null,
tag: r.tag ?? null,
ms: { p50: null, p90: null, p99: null, mean: null },
successRate: r.successRate,
sampleSize: r.sampleSize ?? null,
}))
: b.results
.filter((r) => r.ms.p50 > 0)
.sort((a, c) =>
b.higherIsBetter ? c.ms.p50 - a.ms.p50 : a.ms.p50 - c.ms.p50,
)
.map((r) => ({
name: r.name,
slug: r.slug,
type: r.type ?? null,
layer: r.layer ?? null,
tag: r.tag ?? null,
ms: r.ms,
successRate: r.successRate,
sampleSize: r.sampleSize,
}));

const top = leader(b);
const insufficient = b.dataConfidence === "insufficient";
const payload = {
slug: b.slug,
title: b.title,
subtitle: b.subtitle,
category: b.category,
metric: b.metric,
unit: b.unit,
status,
status: b.status,
higherIsBetter: b.higherIsBetter,
value,
leader: top,
rankings,
sparkline: insufficient ? [] : sparklineFor(b, top?.slug),
sampleSize: insufficient ? 0 : b.sampleSize,
// Aggregate is "insufficient" (median per-provider sample health
// below 10 percent of expected_n): refuse to publish a value or
// leader; the headline is rewritten by headlineSentence so the
// agent / journalist reads "insufficient data" instead of quoting
// a number drawn from undersized samples.
value: insufficient ? null : fieldValue(b),
leader: insufficient ? null : top,
rankings: b.results
.filter((r) => r.ms.p50 > 0)
// Drop "insufficient" rows from the machine-readable ranking too:
// a row that the page hides from the leaderboard must not surface
// here either.
.filter((r) => r.dataConfidence !== "insufficient")
.sort((a, c) => (b.higherIsBetter ? c.ms.p50 - a.ms.p50 : a.ms.p50 - c.ms.p50))
.map((r) => ({
name: r.name,
slug: r.slug,
ms: r.ms,
successRate: r.successRate,
sampleSize: r.sampleSize,
sampleHealth: r.sampleHealth,
dataConfidence: r.dataConfidence,
})),
sparkline: sparklineFor(b, top?.slug),
sampleSize: b.sampleSize,
expectedN: b.expectedN,
dataConfidence: b.dataConfidence,
asOf: b.lastRunAt,
headline: headlineSentence(b),
quote: citationQuote(b, SITE.url),
cite: citeBundle(b, SITE.url),
pageUrl: `${SITE.url}/benchmarks/${b.slug}`,
ogImage: `${SITE.url}/api/og/${b.slug}`,
source: b.source,
methodology: b.methodology,
license: "CC-BY-4.0",
// Per-chain leader / worst. Empty for benches without a chain
// dimension. Used by the HF publisher to populate chain_leaders.
// Insufficient benches null both since the underlying rankings
// are not citable.
bestPerChain: insufficient ? null : (b.bestPerChain ?? null),
worstPerChain: insufficient ? null : (b.worstPerChain ?? null),
};

return NextResponse.json(payload, {
Expand Down
112 changes: 112 additions & 0 deletions src/app/benchmarks/category/[cat]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getBenchmarksSafe, toBenchmarkCardData } from "@/data/benchmarks";
import { BenchmarkGrid } from "@/components/benchmark-grid";
import { Breadcrumb } from "@/components/breadcrumb";
import { safeJsonLd, buildItemListJsonLd } from "@/lib/jsonld";
import { SITE } from "@/data/site";
import { pageMetadata } from "@/lib/page-metadata";
import { capDescription } from "@/lib/seo-text";
import { CATEGORIES, CATEGORY_BY_SLUG } from "@/lib/categories";

/**
* Per-category hub page. One static route per entry in `CATEGORIES` so
* each architectural slice (RPCs, Bridges, Blockchains, ...) has a
* crawlable, linkable URL. Lets external sites and internal nav deep
* link directly into "all Solana RPC benchmarks" instead of relying on
* the client-only filter that lives on `/benchmarks`.
*
* Prerendered at build time via `generateStaticParams` (closed list, no
* Prom calls needed). ISR refresh aligned with the main hub so a freshly
* added bench surfaces here on the same cadence.
*/

export const revalidate = 60;

type Params = { cat: string };

export async function generateStaticParams() {
// Closed enum, generated even when a category currently has zero live
// benches (e.g. Wallets at time of writing). The page-level fallback
// below 404s empty categories so crawlers don't index thin pages.
return CATEGORIES.map((c) => ({ cat: c.slug }));
}

export async function generateMetadata({
params,
}: {
params: Promise<Params>;
}): Promise<Metadata> {
const { cat } = await params;
const entry = CATEGORY_BY_SLUG.get(cat);
if (!entry) return {};
const description = capDescription(entry.description, 158);
return pageMetadata({
path: `/benchmarks/category/${entry.slug}`,
title: `${entry.heading}`,
description,
});
}

export default async function BenchmarkCategoryPage({
params,
}: {
params: Promise<Params>;
}) {
const { cat } = await params;
const entry = CATEGORY_BY_SLUG.get(cat);
if (!entry) notFound();

const all = await getBenchmarksSafe();
const benchmarks = all.filter((b) => b.category === entry.label);
// Empty-category guard: a category in the enum that has no live bench
// yet returns 404 so the crawler doesn't land on a thin page. The
// category still ships in `generateStaticParams` so adding the first
// bench to it lights up the URL without a redeploy gate.
if (benchmarks.length === 0) notFound();

const url = `${SITE.url}/benchmarks/category/${entry.slug}`;
const jsonLd = buildItemListJsonLd({
name: `${entry.heading} on OpenChainBench`,
url,
description: entry.description,
items: benchmarks.map((b) => ({
name: b.title,
url: `${SITE.url}/benchmarks/${b.slug}`,
})),
breadcrumb: [
{ name: "Home", url: `${SITE.url}/` },
{ name: "All benchmarks", url: `${SITE.url}/benchmarks` },
{ name: entry.heading, url },
],
});

return (
<article className="mx-auto max-w-[1400px] px-4 sm:px-6 py-12 sm:py-16">
<script
type="application/ld+json"
// biome-ignore lint/security/noDangerouslySetInnerHtml: serialized via safeJsonLd
dangerouslySetInnerHTML={{ __html: safeJsonLd(jsonLd) }}
/>
<Breadcrumb
items={[
{ label: "Benchmarks", href: "/benchmarks" },
{ label: entry.heading },
]}
/>
<header className="mb-10 mt-4">
<h1 className="display text-4xl sm:text-5xl text-ink">
{entry.heading}
</h1>
<p className="mt-4 max-w-2xl text-base sm:text-lg text-ink-soft leading-snug">
{entry.description}
</p>
</header>
<BenchmarkGrid
benchmarks={benchmarks.map(toBenchmarkCardData)}
lockedCategory={entry.label}
allCategories={Array.from(new Set(all.map((b) => b.category)))}
/>
</article>
);
}
21 changes: 17 additions & 4 deletions src/app/compare/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Metadata } from "next";
import Link from "next/link";
import { ArrowUpRight } from "lucide-react";
import { COMPARE_PAIRS } from "@/data/compare-pairs";
import { canonicalize } from "@/lib/providers";
import { COMPARE_PAIRS, type ComparePair } from "@/data/compare-pairs";
import { canonicalize, getProvider } from "@/lib/providers";
import { buildCompareGraph } from "@/lib/compare-pairing";
import { ProviderLogo } from "@/components/provider-logo";
import { CompareSelector } from "@/components/compare-selector";
Expand All @@ -24,9 +24,22 @@ export const metadata: Metadata = pageMetadata({
});

export default async function ComparePage() {
const pairs = [...COMPARE_PAIRS].sort((a, b) =>
a.slug.localeCompare(b.slug),
// Filter out pairs whose providers don't resolve in the live registry
// (e.g. raydium has no bench appearances yet, so /compare/jupiter-vs-raydium
// 404s downstream). Without this, the hub leaks dead links into both
// the rendered list and the JSON-LD ItemList that Google ingests.
const resolved = await Promise.all(
COMPARE_PAIRS.map(async (pair) => {
const [a, b] = await Promise.all([
getProvider(pair.providerA),
getProvider(pair.providerB),
]);
return a && b ? pair : null;
}),
);
const pairs = resolved
.filter((p): p is ComparePair => p !== null)
.sort((a, b) => a.slug.localeCompare(b.slug));
const graph = await buildCompareGraph();

const jsonld = {
Expand Down
Loading
Loading