From bb6a40b8875e9d18762d2c568bb6fa6729d65ae8 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Thu, 25 Jun 2026 15:31:20 +0200 Subject: [PATCH] =?UTF-8?q?finish=20ton=E2=86=92gram=20migration:=20slug?= =?UTF-8?q?=20aliasing=20+=20overlay=20reconciliation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Diagnostic: after PR #705 (chain slug ton → gram), /chains/gram 404s and /benchmarks/network-fees still renders 'TON' in chain headings. Two root causes: 1. getBenchmarksForChain('gram') filters results[].slug === 'gram', but the materialize worker's KV snapshots still hold the legacy results[].slug === 'ton' (worker on Railway runs from the private mobula-api repo; it hasn't redeployed with the renamed YAML yet). → 0 matches → notFound() at chains/[slug]/page.tsx:90. 2. ChainHeadingsSummary renders r.name verbatim. Stored snapshots still carry name='TON', so the heading reads 'TON native transfer fee usd' even though chains.ts now says label='Gram'. Fix: introduce a chain-slug alias system so the canonical layer absorbs the rebrand without waiting on the harness. - src/lib/chains.ts: - CHAIN_SLUG_ALIASES { ton: 'gram' } — single source of truth. - canonicalChainSlug(slug) and chainLabelForSlug(slug) helpers. - getBenchmarksForChain now matches on the canonical slug + any legacy alias, so /chains/gram surfaces benches whose stored results still carry slug='ton'. - src/lib/spec.ts: - overlayEditorial reconciles stale stored results against the current spec. For each result whose slug is a known legacy alias (e.g. 'ton'), look up the spec provider by canonical slug ('gram'), rewrite result.slug + result.name to match. Downstream surfaces (leaderboard table, search dialog, ChainHeadings) all see the canonical identity without any per-component patch. - Cache keys bumped bench-unfiltered v12 → v13 and all-benchmarks v16 → v17 so the unstable_cache regenerates through the new reconciliation path on deploy. - src/components/chain-headings-summary.tsx: - Anchor id, link URL, and explainer lookup go through canonicalChainSlug so a stale 'ton' result still hits the renamed 'gram' explainer + lands at /benchmarks//gram. - Display name resolves through chainLabelForSlug, with the bench result name as a fallback. Renders 'Gram' the moment chains.ts is updated — no wait on the materialize worker. Test plan: - /chains/gram → 200 with l1-finality + network-fees in the list. - /benchmarks/network-fees → chain heading reads 'Gram transaction fee usd', anchor id='gram', link to /benchmarks/network-fees/gram. - /api/citable still returns rows for both old + new slug surfaces (no impact on machine-readable consumers). - Harness on Railway can stay on chain='ton' label; the existing Prom regex straddle (PR #705) keeps queries hitting both labels. --- src/components/chain-headings-summary.tsx | 37 +++++++++------ src/lib/chains.ts | 57 ++++++++++++++++++++++- src/lib/spec.ts | 33 ++++++++++++- 3 files changed, 110 insertions(+), 17 deletions(-) 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);