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
37 changes: 24 additions & 13 deletions src/components/chain-headings-summary.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,10 @@ import Link from "next/link";
import type { Benchmark } from "@/types/benchmark";
import { liveResults } from "@/lib/provider-filters";
import { fmtUnit } from "@/lib/format";
import {
canonicalChainSlug,
chainLabelForSlug,
} from "@/lib/chains";

/**
* Server-rendered per-provider H2 block. Each chain on a chain-shaped
Expand DownExpand Up@@ -35,11 +39,14 @@ export function ChainHeadingsSummary({ benchmark }: { benchmark: Benchmark }) {
benchmark.higherIsBetter ? b.ms.p50 - a.ms.p50 : a.ms.p50 - b.ms.p50
);

// Look up per-chain explainer by slug. Map for O(1) access from the
// sort loop. When present, the slug's body is rendered as a second
// paragraph below the live p50 line.
// Look up per-chain explainer by slug. Keyed by canonical slug so a
// stale result.slug ("ton") still finds the renamed explainer ("gram")
// during a rebrand transition.
const explainerBySlug = new Map(
(benchmark.perChainExplainer ?? []).map((e) => [e.slug, e])
(benchmark.perChainExplainer ?? []).map((e) => [
canonicalChainSlug(e.slug),
e,
])
);

return (
Expand All@@ -59,23 +66,27 @@ export function ChainHeadingsSummary({ benchmark }: { benchmark: Benchmark }) {

<div className="mt-8 space-y-8">
{sorted.map((r) => {
const explainer = explainerBySlug.get(r.slug);
// Heading: prefer the YAML-declared H2 string when present (it
// can phrase the heading more naturally than the default
// "{name} {metric}" template). Each heading gets an id={slug}
// so URLs like /benchmarks/l1-finality#ethereum land at the
// exact section, which directly answers GSC long-tail queries.
// Resolve a stale result.slug ("ton") to its current canonical
// ("gram") for anchor id, link URL, and explainer lookup.
// Display name prefers the chain registry's label over the
// bench result's own name field, so a stale "TON" row reads
// as "Gram" the moment chains.ts is updated — no need to wait
// for the materialize snapshot to refresh.
const canonSlug = canonicalChainSlug(r.slug);
const explainer = explainerBySlug.get(canonSlug);
const displayName = chainLabelForSlug(r.slug) ?? r.name;
const heading =
explainer?.h2 ?? `${r.name} ${benchmark.metric.toLowerCase()}`;
explainer?.h2 ??
`${displayName} ${benchmark.metric.toLowerCase()}`;
return (
<article key={r.slug} id={r.slug} className="scroll-mt-20">
<article key={canonSlug} id={canonSlug} className="scroll-mt-20">
<h2 className="display text-xl tracking-tight text-ink">
{/* Chains with an explainer have a dedicated landing page
(/benchmarks/<slug>/<chain>); the heading links there so
crawlers discover the per-chain documents from the hub. */}
{explainer ? (
<Link
href={`/benchmarks/${benchmark.slug}/${r.slug}`}
href={`/benchmarks/${benchmark.slug}/${canonSlug}`}
className="hover:underline underline-offset-4"
>
{heading}
Expand Down
57 changes: 55 additions & 2 deletions src/lib/chains.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -207,6 +207,48 @@ export const CHAINS: ChainEntry[] = [

export const CHAIN_BY_SLUG = new Map(CHAINS.map((c) => [c.slug, c]));

/**
* Legacy chain slugs that should resolve to a current canonical chain.
*
* Necessary because a slug rename (e.g. TON token rebrand to Gram in
* June 2026) leaves stale identifiers in three places that don't all
* roll over at the same time:
* - YAML provider entries that haven't been edited yet
* - Cached Benchmark snapshots in Upstash KV (populated by the
* materialize worker before the rename, results[].slug = old)
* - Harness Prom labels still emitting the old chain label
*
* `getBenchmarksForChain` honours these aliases so the new canonical
* /chains/<gram> URL still surfaces benches whose results[].slug is the
* old "ton" until everything downstream catches up. ChainHeadingsSummary
* and any other component reading r.slug uses `chainLabelForSlug` to
* resolve the alias to the registry's display label, so a stale "TON"
* row reads as "Gram" anywhere the chain registry can override it.
*
* Add a new alias when a chain rebrands; remove an alias once the
* caches and harnesses have rotated past it.
*/
export const CHAIN_SLUG_ALIASES: Record<string, string> = {
ton: "gram",
};

/** Map any slug to its canonical chain slug. Identity for known slugs,
* resolves a legacy slug to its current canonical via CHAIN_SLUG_ALIASES,
* returns the input lowercased for anything unknown. */
export function canonicalChainSlug(slug: string): string {
const lc = slug.toLowerCase();
return CHAIN_SLUG_ALIASES[lc] ?? lc;
}

/** Display label for a slug, resolving aliases against the chain
* registry. Returns null when neither the canonical nor the raw slug
* is registered, so callers can fall back to whatever local data they
* have (e.g. the bench result's own name field). */
export function chainLabelForSlug(slug: string): string | null {
const canon = canonicalChainSlug(slug);
return CHAIN_BY_SLUG.get(canon)?.label ?? null;
}

/**
* Returns the list of benchmarks that surface this chain in some way:
*
Expand All@@ -226,9 +268,20 @@ export const getBenchmarksForChain = cache(async function getBenchmarksForChain(
chainSlug: string,
): Promise<Benchmark[]> {
const benches = await getBenchmarksSafe();
// Build the set of slugs that should resolve as this chain: the input
// itself plus any legacy slug that aliases TO it. This lets the new
// canonical /chains/<gram> URL still find benches whose results or
// dimensions still carry the legacy "ton" slug while the YAMLs +
// materialize snapshots + harness labels rotate over.
const canon = canonicalChainSlug(chainSlug);
const accept = new Set<string>([canon]);
for (const [legacy, target] of Object.entries(CHAIN_SLUG_ALIASES)) {
if (target === canon) accept.add(legacy);
}
return benches.filter((b) => {
if (b.results.some((r) => r.slug === chainSlug)) return true;
if (b.dimensions?.chain?.some((c) => c.value === chainSlug)) return true;
if (b.results.some((r) => accept.has(r.slug.toLowerCase()))) return true;
if (b.dimensions?.chain?.some((c) => accept.has(c.value.toLowerCase())))
return true;
return false;
});
});
33 changes: 31 additions & 2 deletions src/lib/spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ import { cache } from "react";
import { unstable_cache } from "next/cache";
import type { Benchmark } from "@/types/benchmark";
import type { Spec } from "@/lib/spec-schema";
import { canonicalChainSlug } from "@/lib/chains";
import { renderBenchmarkText } from "@/lib/bench-template";
import {
buildEditorial,
Expand DownExpand Up@@ -71,8 +72,28 @@ async function benchFromStore(
* untouched.
*/
function overlayEditorial(stored: Benchmark, spec: Spec): Benchmark {
// Reconcile stale provider entries in the stored snapshot against the
// current spec. The materialize worker may have written a snapshot
// BEFORE a chain rename rolled through the YAMLs (e.g. ton → gram).
// For each stored result whose slug is a known legacy alias, look up
// the spec provider for the canonical slug and rewrite the result's
// `slug` + `name` so downstream surfaces (leaderboard table, chain
// hub matching, search dialog) read the new identity without having
// to wait on the worker's next sweep.
const providerByCanonSlug = new Map(
(spec.providers ?? []).map((p) => [canonicalChainSlug(p.slug), p]),
);
const reconciledResults = stored.results.map((r) => {
const canon = canonicalChainSlug(r.slug);
if (canon === r.slug) return r;
const specProvider = providerByCanonSlug.get(canon);
if (!specProvider) return r;
return { ...r, slug: canon, name: specProvider.name };
});

const overlaid: Benchmark = {
...stored,
results: reconciledResults,
seoTitle: spec.seo_title ?? stored.seoTitle,
seoDescription: spec.seo_description ?? stored.seoDescription,
seoIntro: spec.seo_intro ?? stored.seoIntro,
Expand DownExpand Up@@ -195,7 +216,12 @@ const loadBenchmarkUnfilteredCached = unstable_cache(
// in client RSC payload + leaked via /api/citable → search dialog
// description previews). Bump so the next read regenerates through
// the wider resolver.
["bench-unfiltered-v12"],
// v13: overlayEditorial now reconciles stale provider slugs in stored
// snapshots against the current spec (e.g. legacy "ton" → canonical
// "gram" after the June 2026 rebrand). Without bumping, a v12 entry
// would keep serving result.slug = "ton" + result.name = "TON" until
// the materialize worker rewrites the snapshot.
["bench-unfiltered-v13"],
{ revalidate: 300, tags: ["benchmarks"] },
);

Expand DownExpand Up@@ -323,7 +349,10 @@ const loadAllBenchmarksCached = unstable_cache(
// coverage, search-bar PR). Without bumping this, products / citable /
// sitemap surfaces would keep serving v15 benches with raw
// `{{best_name}}` in seoDescription for up to 300s after deploy.
["all-benchmarks-v16"],
// v17: bumped with bench-unfiltered-v13 (slug reconciliation in
// overlayEditorial). Without this, /api/citable and the products
// page would keep serving v16 benches with stale "ton" results.
["all-benchmarks-v17"],
{ revalidate: 300, tags: ["benchmarks"] },
);
export const loadAllBenchmarks = cache(loadAllBenchmarksCached);
Expand Down