diff --git a/src/components/chain-headings-summary.tsx b/src/components/chain-headings-summary.tsx
index 75f7c6c2..180bf300 100644
--- a/src/components/chain-headings-summary.tsx
+++ b/src/components/chain-headings-summary.tsx
@@ -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
@@ -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 (
@@ -59,23 +66,27 @@ export function ChainHeadingsSummary({ benchmark }: { benchmark: Benchmark }) {
{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 (
-
+
{/* Chains with an explainer have a dedicated landing page
(/benchmarks//); the heading links there so
crawlers discover the per-chain documents from the hub. */}
{explainer ? (
{heading}
diff --git a/src/lib/chains.ts b/src/lib/chains.ts
index 875a073f..af34e3fb 100644
--- a/src/lib/chains.ts
+++ b/src/lib/chains.ts
@@ -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/ 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 = {
+ 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:
*
@@ -226,9 +268,20 @@ export const getBenchmarksForChain = cache(async function getBenchmarksForChain(
chainSlug: string,
): Promise {
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/ 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([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;
});
});
diff --git a/src/lib/spec.ts b/src/lib/spec.ts
index 58bd290a..d5ea0e90 100644
--- a/src/lib/spec.ts
+++ b/src/lib/spec.ts
@@ -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,
@@ -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,
@@ -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"] },
);
@@ -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);