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
16 changes: 13 additions & 3 deletions src/app/api/badge/[slug]/[provider]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { fmtUnit } from "@/lib/format";
import { readBestPerChain } from "@/lib/per-chain-contract";
import { clientKey, rateLimit, tooManyRequests } from "@/lib/rate-limit";
import { PROVIDER_RE, SLUG_RE } from "@/lib/slug";
import { matchesChainSlug } from "@/lib/chain-aliases";
import type { Benchmark, ProviderResult } from "@/types/benchmark";

export const revalidate = 300;
Expand Down Expand Up @@ -144,9 +145,14 @@ function truncate(s: string, max: number): string {
return s.slice(0, max - 1).trimEnd() + "…";
}

/** Returns the human label for a chain value from the bench's spec. */
/** Returns the human label for a chain value from the bench's spec.
* Canonical-aware so a request with the new slug ("gram") still finds
* the dimension entry whose value is the legacy "ton". */
function chainLabel(b: Benchmark, chain: string): string {
return b.dimensions?.chain?.find((c) => c.value === chain)?.label ?? chain;
return (
b.dimensions?.chain?.find((c) => matchesChainSlug(c.value, chain))?.label ??
chain
);
}

/** Returns the human label for a region value from the bench's spec. */
Expand Down Expand Up @@ -185,8 +191,12 @@ export async function GET(
const url = new URL(req.url);
const rawChain = url.searchParams.get("chain")?.toLowerCase().trim() || null;
const rawRegion = url.searchParams.get("region")?.toLowerCase().trim() || null;
// Resolve via the alias-aware matcher so /api/badge?chain=gram lands
// on the dimension whose YAML value is "ton" (kept to match the
// harness's Prom labels). Returns the actual dimension value so the
// downstream cell lookup hits the snapshot's storage key.
const chainParam = rawChain
? (b.dimensions?.chain?.find((c) => c.value.toLowerCase() === rawChain)
? (b.dimensions?.chain?.find((c) => matchesChainSlug(c.value, rawChain))
?.value ?? null)
: null;
const regionParam = rawRegion
Expand Down
10 changes: 8 additions & 2 deletions src/app/api/bench/[slug]/variant/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { type NextRequest, NextResponse } from "next/server";
import { getBenchmark } from "@/data/benchmarks";
import { clientKey, rateLimit, tooManyRequests } from "@/lib/rate-limit";
import { SLUG_RE } from "@/lib/slug";
import { matchesChainSlug } from "@/lib/chain-aliases";

export const revalidate = 60;

Expand Down Expand Up @@ -49,8 +50,13 @@ export async function GET(
for (const dim of ["chain", "region", "kind"] as const) {
const raw = url.searchParams.get(dim)?.toLowerCase().trim();
if (!raw || raw === "all") continue;
const known = (aggregate.dimensions?.[dim] ?? []).find(
(d) => d.value.toLowerCase() === raw,
// Canonical-aware matching: the chain dimension may still hold the
// legacy slug ("ton") even though clients now request the canonical
// ("gram"). The matcher resolves both sides to canonical.
const known = (aggregate.dimensions?.[dim] ?? []).find((d) =>
dim === "chain"
? matchesChainSlug(d.value, raw)
: d.value.toLowerCase() === raw,
);
if (!known) {
return new NextResponse(`unknown ${dim}`, {
Expand Down
30 changes: 23 additions & 7 deletions src/app/benchmarks/[slug]/[chain]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
import { SITE } from "@/data/site";
import { buildBreadcrumbJsonLd, safeJsonLd } from "@/lib/jsonld";
import { CATEGORY_COLOR } from "@/lib/category-colors";
import {
canonicalChainSlug,

Check warning on line 14 in src/app/benchmarks/[slug]/[chain]/page.tsx

View workflow job for this annotation

GitHub Actions / check

'canonicalChainSlug' is defined but never used
matchesChainSlug,
} from "@/lib/chain-aliases";
import type { Benchmark, ProviderResult } from "@/types/benchmark";

// Dedicated per-chain landing pages. Only chains that have a hand-written
Expand Down Expand Up @@ -119,8 +123,12 @@
): Promise<ChainPageData | null> {
const benchmark = await getBenchmark(slug);
if (!benchmark) return null;
const found = (benchmark.perChainExplainer ?? []).find(
(e) => e.slug === chain,
// Canonical-aware lookups: a URL like /benchmarks/<bench>/gram must
// resolve when the bench's perChainExplainer / results / dimensions
// still carry the legacy slug ("ton") because YAMLs and the harness
// haven't all rotated past the rename yet.
const found = (benchmark.perChainExplainer ?? []).find((e) =>
matchesChainSlug(e.slug, chain),
);
if (!found) return null;
const explainer = {
Expand All @@ -130,19 +138,24 @@
};

// Shape 1: the chain is a leaderboard row (l1-finality).
const result = benchmark.results.find((r) => r.slug === chain);
const result = benchmark.results.find((r) => matchesChainSlug(r.slug, chain));
if (result) {
const sorted = sortLive(benchmark.results, benchmark.higherIsBetter);
const rank = sorted.findIndex((r) => r.slug === chain) + 1;
const rank = sorted.findIndex((r) => matchesChainSlug(r.slug, chain)) + 1;
return { shape: "row", benchmark, explainer, result, sorted, rank };
}

// Shape 2: the chain is a filter dimension (rpc-capabilities).
const chainOption = (benchmark.dimensions?.chain ?? []).find(
(c) => c.value === chain && c.value.toLowerCase() !== "all",
(c) => matchesChainSlug(c.value, chain) && c.value.toLowerCase() !== "all",
);
if (!chainOption) return null;
const scoped = (await getBenchmark(slug, { chain })) ?? benchmark;
// Use the actual YAML value (not the canonical) when fetching the
// scoped variant — the Prom-label injection downstream expects the
// raw dimension value (e.g. "ton") so the regex straddle keeps
// matching the harness's current label.
const scoped =
(await getBenchmark(slug, { chain: chainOption.value })) ?? benchmark;
const providers = sortLive(scoped.results, benchmark.higherIsBetter);
const leader = providers[0] ?? null;

Expand All @@ -157,7 +170,10 @@
const variants = await Promise.all(
regions.map(async (r) => ({
label: r.label,
bench: await getBenchmark(slug, { chain, region: r.value }),
bench: await getBenchmark(slug, {
chain: chainOption.value,
region: r.value,
}),
})),
);
for (const v of variants) {
Expand Down
4 changes: 3 additions & 1 deletion src/app/benchmarks/[slug]/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getBenchmark } from "@/data/benchmarks";
import { headlineSentence, leader } from "@/lib/citation";
import { fmtUnit } from "@/lib/format";
import { CATEGORY_COLOR } from "@/lib/category-colors";
import { matchesChainSlug } from "@/lib/chain-aliases";
import { loadBenchmark } from "@/lib/spec";

export const runtime = "nodejs";
Expand Down Expand Up @@ -76,7 +77,8 @@ export default async function OG({
const sentence = headlineSentence(b);
const catColor = CATEGORY_COLOR[b.category] ?? "#7a2e1f";
const chainLabel = chainId
? b.dimensions?.chain?.find((c) => c.value === chainId)?.label ?? chainId
? b.dimensions?.chain?.find((c) => matchesChainSlug(c.value, chainId))
?.label ?? chainId
: null;
const titleText = chainLabel ? `${b.title} on ${chainLabel}` : b.title;

Expand Down
3 changes: 2 additions & 1 deletion src/app/benchmarks/[slug]/share-card/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getBenchmark } from "@/data/benchmarks";
import { buildProviderColors } from "@/lib/series-colors";
import { fmtUnit, fmtValue, unitSuffix } from "@/lib/format";
import { logoPath } from "@/lib/logo-manifest";
import { matchesChainSlug } from "@/lib/chain-aliases";
import { chipBackground, chipTextColor, initials } from "@/lib/brand";
import type { Benchmark, ProviderResult } from "@/types/benchmark";
import { clientKey, rateLimit, tooManyRequests } from "@/lib/rate-limit";
Expand Down Expand Up @@ -527,7 +528,7 @@ export async function GET(
const isAll = chainParam === "all";
const chainOption = isAll
? null
: chainOptions.find((c) => c.value === chainParam) ?? null;
: chainOptions.find((c) => matchesChainSlug(c.value, chainParam)) ?? null;
const benchmark = chainOption
? (await getBenchmark(slug, { chain: chainOption.value })) ?? aggregate
: aggregate;
Expand Down
4 changes: 3 additions & 1 deletion src/app/benchmarks/[slug]/twitter-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getBenchmark } from "@/data/benchmarks";
import { headlineSentence, leader } from "@/lib/citation";
import { fmtUnit } from "@/lib/format";
import { CATEGORY_COLOR } from "@/lib/category-colors";
import { matchesChainSlug } from "@/lib/chain-aliases";
import { loadBenchmark } from "@/lib/spec";

export const runtime = "nodejs";
Expand Down Expand Up @@ -66,7 +67,8 @@ export default async function TwitterImage({
const sentence = headlineSentence(b);
const catColor = CATEGORY_COLOR[b.category] ?? "#7a2e1f";
const chainLabel = chainId
? b.dimensions?.chain?.find((c) => c.value === chainId)?.label ?? chainId
? b.dimensions?.chain?.find((c) => matchesChainSlug(c.value, chainId))
?.label ?? chainId
: null;
const titleText = chainLabel ? `${b.title} on ${chainLabel}` : b.title;

Expand Down
18 changes: 13 additions & 5 deletions src/app/chains/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ProviderLogo } from "@/components/provider-logo";
import { SITE } from "@/data/site";
import { buildBreadcrumbJsonLd, safeJsonLd } from "@/lib/jsonld";
import { capDescription } from "@/lib/seo-text";
import { matchesChainSlug } from "@/lib/chain-aliases";
import type { Benchmark } from "@/types/benchmark";

export const revalidate = 60;
Expand Down Expand Up @@ -200,12 +201,19 @@ export default async function ChainPage({
</h2>
<ul className="mt-4 divide-y divide-rule border-y border-rule">
{list.map((b) => {
const ownResult = b.results.find((r) => r.slug === slug);
const chainOption = b.dimensions?.chain?.find(
(c) => c.value === slug,
// Canonical-aware matches: the bench's results / dimensions /
// perChainExplainer might still carry the legacy slug ("ton")
// while the URL is the renamed "gram". The matcher resolves
// both sides to the canonical chain so all three lookups land.
const ownResult = b.results.find((r) =>
matchesChainSlug(r.slug, slug),
);
const chainOption = b.dimensions?.chain?.find((c) =>
matchesChainSlug(c.value, slug),
);
const hasChainRoute = (b.perChainExplainer ?? []).some((e) =>
matchesChainSlug(e.slug, slug),
);
const hasChainRoute =
(b.perChainExplainer ?? []).some((e) => e.slug === slug);
const href = hasChainRoute
? `/benchmarks/${b.slug}/${slug}`
: `/benchmarks/${b.slug}`;
Expand Down
19 changes: 14 additions & 5 deletions src/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getBenchmarks } from "@/data/benchmarks";
import { loadAllAlternatives } from "@/lib/alternatives";
import { loadAllAnswers } from "@/lib/answers";
import { CHAINS, getBenchmarksForChain } from "@/lib/chains";
import { canonicalChainSlug } from "@/lib/chain-aliases";
import { getProvider, getProviderSlugs } from "@/lib/providers";
import { SITE } from "@/data/site";
import type { Benchmark } from "@/types/benchmark";
Expand Down Expand Up @@ -174,16 +175,24 @@ async function buildFullSitemap(): Promise<MetadataRoute.Sitemap> {
priority: 0.95,
},
];
const resultSlugs = new Set(b.results.map((r) => r.slug));
// Canonicalize at insertion + check time so a chain rebrand window
// (where YAML dimension still has the legacy value "ton" while
// perChainExplainer + chain registry have moved to "gram") doesn't
// drop the new URLs from the sitemap. Emit the canonical URL so
// crawlers never index legacy /ton paths that 308 to /gram.
const resultSlugs = new Set(
b.results.map((r) => canonicalChainSlug(r.slug)),
);
const chainValues = new Set(
(b.dimensions?.chain ?? [])
.map((c) => c.value)
.filter((v) => v.toLowerCase() !== "all"),
.filter((c) => c.value.toLowerCase() !== "all")
.map((c) => canonicalChainSlug(c.value)),
);
for (const e of b.perChainExplainer ?? []) {
if (!resultSlugs.has(e.slug) && !chainValues.has(e.slug)) continue;
const canon = canonicalChainSlug(e.slug);
if (!resultSlugs.has(canon) && !chainValues.has(canon)) continue;
entries.push({
url: `${SITE.url}/benchmarks/${b.slug}/${e.slug}`,
url: `${SITE.url}/benchmarks/${b.slug}/${canon}`,
lastModified: last,
changeFrequency: "hourly",
priority: 0.85,
Expand Down
7 changes: 6 additions & 1 deletion src/components/benchmark-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useSearchParams } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import type { Benchmark } from "@/types/benchmark";
import { liveResults } from "@/lib/provider-filters";
import { matchesChainSlug } from "@/lib/chain-aliases";
import { ChainTabs } from "@/components/chain-tabs";
import { LedgerTable } from "@/components/ledger-table";
import { TimeSeriesChart } from "@/components/time-series-chart";
Expand Down Expand Up @@ -154,8 +155,12 @@ export function BenchmarkBody({
const urlRegion = searchParams.get("region");
const urlKind = searchParams.get("kind");
const urlLayer = searchParams.get("layer");
// Canonical-aware lookup: a URL with the new slug ("?chain=gram")
// still selects the dimension whose YAML value is the legacy "ton".
const resolvedInitialChain =
(urlChain && chainOptions.find((c) => c.value === urlChain)?.value) ?? initialChain;
(urlChain &&
chainOptions.find((c) => matchesChainSlug(c.value, urlChain))?.value) ??
initialChain;
const resolvedInitialRegion =
(urlRegion && regionOptions.find((r) => r.value === urlRegion)?.value) ?? initialRegion;
const resolvedInitialKind =
Expand Down
27 changes: 27 additions & 0 deletions src/lib/chain-aliases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,30 @@ export function canonicalChainSlug(slug: string): string {
const lc = slug.toLowerCase();
return CHAIN_SLUG_ALIASES[lc] ?? lc;
}

/** Returns every legacy slug that aliases to the given canonical, plus
* the canonical itself. Use as a Set when filtering YAML dimensions /
* result rows whose slug may still be in legacy form. */
export function chainSlugSiblings(slug: string): Set<string> {
const canon = canonicalChainSlug(slug);
const set = new Set<string>([canon]);
for (const [legacy, target] of Object.entries(CHAIN_SLUG_ALIASES)) {
if (target === canon) set.add(legacy);
}
return set;
}

/** True when two slugs refer to the same chain — either both canonical,
* one legacy that aliases to the other, or both legacy mapping to the
* same canonical. The canonical chain-comparison helper used at every
* site that previously did `=== chain` (route handlers, OG generators,
* badge API, sitemap filters, etc.). Case-insensitive. Both args are
* optional so callers with `string | null` URL params can pass without
* an inline guard. */
export function matchesChainSlug(
a: string | null | undefined,
b: string | null | undefined,
): boolean {
if (!a || !b) return false;
return canonicalChainSlug(a) === canonicalChainSlug(b);
}
Loading