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
110 changes: 110 additions & 0 deletions src/lib/categories.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
/**
* Category taxonomy for benchmark hubs.
*
* Mirrors the closed `Category` enum from `spec-schema.ts` (the source
* of truth that every YAML spec gets validated against). Centralised
* here so the `/benchmarks/category/<cat>` routes, the sitemap, the
* client filter and the meta-builders all share one slug ↔ label ↔
* description vocabulary. If a new category is added to the schema,
* adding it here is the only other touch-point needed to surface a
* dedicated hub URL.
*
* Slugs are lowercase, kebab-case, ASCII-only so they survive URL
* canonicalisation, RSS readers and JSON-LD @id fragments without
* needing encoding. Labels match the schema enum verbatim so the
* client filter can compare `b.category === entry.label` without an
* extra lookup.
*
* Descriptions are SEO copy that explains the category scope. They
* land in <meta description>, OG description, and the H1 sub-paragraph
* on the category hub page, so each must read as a standalone sentence
* (a) without the surrounding template and (b) with the category name
* substituted in. No em / en dashes per the project copy rules.
*/

export type Category =
| "Aggregators"
| "Bridges"
| "Blockchains"
| "Trading"
| "Wallets"
| "RPCs"
| "NFT APIs";

export type CategoryEntry = {
/** URL slug. Lowercase, kebab-case, ASCII. */
slug: string;
/** Schema enum label, matches `Benchmark.category` exactly. */
label: Category;
/** Plural human heading used in H1 + breadcrumb. */
heading: string;
/** SEO copy under the H1 + <meta description>. */
description: string;
};

export const CATEGORIES: readonly CategoryEntry[] = [
{
slug: "aggregators",
label: "Aggregators",
heading: "Aggregator benchmarks",
description:
"Live OpenChainBench measurements for crypto data aggregators. Compare quote latency, market coverage, and freshness across the providers that fan out queries to multiple underlying sources.",
},
{
slug: "bridges",
label: "Bridges",
heading: "Bridge benchmarks",
description:
"Open, reproducible benchmarks for cross-chain bridges and intent layers. Compare quote latency, end-to-end fees, and route coverage across the bridges that move value between chains.",
},
{
slug: "blockchains",
label: "Blockchains",
heading: "Blockchain benchmarks",
description:
"Network-level measurements that compare blockchains head to head. Time to finality, block time, gas estimation accuracy, and validator economics on every L1 and L2 OpenChainBench tracks.",
},
{
slug: "trading",
label: "Trading",
heading: "Trading benchmarks",
description:
"Latency, fee, and freshness benchmarks for trading venues and execution providers. Perpetuals, spot DEX quotes, oracle deviation, and Polymarket data freshness measured continuously.",
},
{
slug: "wallets",
label: "Wallets",
heading: "Wallet benchmarks",
description:
"Benchmarks that compare wallet infrastructure providers on label coverage, address resolution, and the data layers that power production wallet UIs.",
},
{
slug: "rpcs",
label: "RPCs",
heading: "RPC benchmarks",
description:
"Live OpenChainBench measurements for RPC and node providers. Compare capability coverage, transaction landing latency, and reliability across the endpoints that power production dApps.",
},
{
slug: "nft-apis",
label: "NFT APIs",
heading: "NFT API benchmarks",
description:
"Benchmarks that compare NFT data API providers on collection metadata coverage, freshness, and the indexing depth that production NFT marketplaces and wallets depend on.",
},
];

export const CATEGORY_BY_SLUG: ReadonlyMap<string, CategoryEntry> = new Map(
CATEGORIES.map((c) => [c.slug, c]),
);

export const CATEGORY_SLUG_BY_LABEL: ReadonlyMap<Category, string> = new Map(
CATEGORIES.map((c) => [c.label, c.slug]),
);

/** Slug used in the `/benchmarks/category/<slug>` URL for a given
* `Benchmark.category` value. Returns `null` for unknown labels so
* call sites can degrade to the unscoped hub instead of a 404 link. */
export function categorySlugFromLabel(label: string): string | null {
return CATEGORY_SLUG_BY_LABEL.get(label as Category) ?? null;
}
5 changes: 3 additions & 2 deletions src/lib/materialize/load.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@ export type BenchmarkFilters = {
chain?: string;
region?: string;
kind?: string;
venue?: string;
};

export function filterSig(f: BenchmarkFilters): string {
Expand All@@ -58,8 +59,8 @@ export function parseFilterSig(sig: string): BenchmarkFilters {
if (!sig) return out;
for (const kv of sig.split("&")) {
const [k, v] = kv.split("=");
if (k && v && (k === "chain" || k === "region" || k === "kind")) {
out[k as "chain" | "region" | "kind"] = v;
if (k && v && (k === "chain" || k === "region" || k === "kind" || k === "venue")) {
out[k as "chain" | "region" | "kind" | "venue"] = v;
}
}
return out;
Expand Down
8 changes: 8 additions & 0 deletions src/lib/spec-schema.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -344,6 +344,14 @@ export const SpecSchema = z
})
)
.optional(),
venue: z
.array(
z.object({
value: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/),
label: z.string().min(1).max(64),
})
)
.optional(),
})
.optional(),

Expand Down
Loading