From fff447d55afb6ae4ba0dfa572c3e2774c311854d Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Fri, 26 Jun 2026 15:01:11 +0200 Subject: [PATCH] feat(badges): public catalog page for embedding live rankings The /api/badge// SVG route and its /snippet helper have been live for months but have no discovery surface. Visitors who want to embed their rank on a README or doc had to read source. /badges is the shields.io-style catalog: search, filter by category, preview every (benchmark, provider) pair, copy a Markdown / HTML / URL / JSON snippet for any of them. Chain and region scope dropdowns are exposed when the parent bench declares those dimensions. Each embed is one more permanent backlink from the provider's README or docs site, so this page should also move the needle on referring domains. --- src/app/badges/page.tsx | 226 ++++++++++++++ src/app/sitemap.ts | 1 + src/components/badges-catalog.tsx | 494 ++++++++++++++++++++++++++++++ src/components/site-footer.tsx | 1 + 4 files changed, 722 insertions(+) create mode 100644 src/app/badges/page.tsx create mode 100644 src/components/badges-catalog.tsx diff --git a/src/app/badges/page.tsx b/src/app/badges/page.tsx new file mode 100644 index 00000000..0318756e --- /dev/null +++ b/src/app/badges/page.tsx @@ -0,0 +1,226 @@ +import Link from "next/link"; +import type { Metadata } from "next"; +import { getBenchmarksSafe } from "@/data/benchmarks"; +import { BadgesCatalog, type BadgePair } from "@/components/badges-catalog"; +import { pageMetadata } from "@/lib/page-metadata"; +import { safeJsonLd, buildBreadcrumbJsonLd } from "@/lib/jsonld"; +import { SITE } from "@/data/site"; + +/** + * Public discovery page for the per-(benchmark, provider) badge endpoints. + * + * The /api/badge// route has been live for months and + * powers per-row embed buttons on bench pages and the /partners docs, + * but nothing on the site lets a visitor browse what is available. + * This page is the shields.io-style catalog: search, filter, preview, + * copy. Also doubles as a backlink generator: every embedded snippet + * is one more link back to a bench page. + * + * Loads from `getBenchmarksSafe` so a Prom blackout still renders the + * shell with whatever the carry-forward snapshot holds. Pairs that + * have no live rank are filtered out so the page never advertises a + * "rank N/A" badge. + */ + +export const revalidate = 600; + +const DESCRIPTION = + "Browse every live ranking badge published by OpenChainBench. Search by provider or category, preview the SVG, then copy a Markdown, HTML, URL or JSON snippet for your README, docs or marketing page. Free, CC-BY-4.0."; + +export const metadata: Metadata = pageMetadata({ + path: "/badges", + title: "Live ranking badges", + description: DESCRIPTION, +}); + +/** Hand-picked example pairs surfaced in the hero preview row. Each one + * is a recognisable leader the page can safely showcase. Pairs missing + * from the live snapshot (provider absent, bench in draft) are silently + * skipped so a transient outage doesn't break the layout. */ +const PREVIEW_PAIRS: Array<{ bench: string; provider: string }> = [ + { bench: "bridge-fee", provider: "mobula" }, + { bench: "perp-fees", provider: "lighter" }, + { bench: "rpc-capabilities", provider: "drpc" }, + { bench: "nft-collection-metadata", provider: "moralis" }, + { bench: "pm-api-latency", provider: "polymarket" }, +]; + +export default async function BadgesPage() { + const benchmarks = await getBenchmarksSafe(); + + const live = benchmarks.filter((b) => b.editorialStatus === "live"); + + // Flat list of (bench, provider) pairs the catalog can browse. Each + // entry carries the ranked position so the card can flash "#1/12" + // without an extra fetch. Sort order: best ranks first inside each + // bench, benches grouped alphabetically by category then title. + const pairs: BadgePair[] = []; + for (const b of live) { + const livePr = b.results.filter((r) => r.ms.p50 > 0); + if (livePr.length === 0) continue; + const sorted = [...livePr].sort((a, c) => + b.higherIsBetter ? c.ms.p50 - a.ms.p50 : a.ms.p50 - c.ms.p50, + ); + sorted.forEach((r, idx) => { + pairs.push({ + benchSlug: b.slug, + benchTitle: b.title, + benchCategory: b.category, + providerSlug: r.slug, + providerName: r.name, + rank: idx + 1, + total: sorted.length, + dimensions: { + chain: b.dimensions?.chain, + region: b.dimensions?.region, + }, + }); + }); + } + pairs.sort((a, b) => { + if (a.benchCategory !== b.benchCategory) + return a.benchCategory.localeCompare(b.benchCategory); + if (a.benchTitle !== b.benchTitle) + return a.benchTitle.localeCompare(b.benchTitle); + return a.rank - b.rank; + }); + + // Lookup table so the hero preview row can resolve the canonical + // provider name + alt text without a second pass over the snapshot. + const pairKey = (bench: string, provider: string) => `${bench}|${provider}`; + const pairIndex = new Map( + pairs.map((p) => [pairKey(p.benchSlug, p.providerSlug), p]), + ); + const previewItems = PREVIEW_PAIRS.map((p) => + pairIndex.get(pairKey(p.bench, p.provider)), + ).filter((p): p is BadgePair => Boolean(p)); + + const collectionLd = { + "@context": "https://schema.org", + "@type": "CollectionPage", + name: "OpenChainBench live ranking badges", + description: DESCRIPTION, + url: `${SITE.url}/badges`, + isPartOf: { + "@type": "WebSite", + name: SITE.name, + url: SITE.url, + }, + mainEntity: { + "@type": "ItemList", + name: "OpenChainBench badges", + numberOfItems: pairs.length, + itemListElement: pairs.slice(0, 100).map((p, i) => ({ + "@type": "ListItem", + position: i + 1, + name: `${p.providerName} on ${p.benchTitle}`, + url: `${SITE.url}/api/badge/${p.benchSlug}/${p.providerSlug}`, + })), + }, + }; + + const breadcrumbLd = { + "@context": "https://schema.org", + ...buildBreadcrumbJsonLd([ + { name: "Home", item: SITE.url }, + { name: "Badges", item: `${SITE.url}/badges` }, + ]), + }; + + return ( +
+