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
11 changes: 8 additions & 3 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,24 @@ const RELAY_WS = "wss://stream.openchainbench.com";
// modal's <video> tag can play the result, and in connect-src so the
// proxy's success URL can be opportunistically pre-fetched.
const VIDEO_RENDERER_ORIGIN = "https://video.openchainbench.com";
// Ahrefs Web Analytics. Loaded from analytics.ahrefs.com and beacons
// back to the same origin — needs both script-src and connect-src
// allowlist entries. Kept as a distinct constant so a future analytics
// swap is a one-line change.
const AHREFS_ORIGIN = "https://analytics.ahrefs.com";
const IS_DEV = process.env.NODE_ENV !== "production";
// React dev mode needs 'unsafe-eval' for fast refresh / call stack reconstruction.
// In prod we keep the lock-tight policy.
const SCRIPT_SRC = IS_DEV
? "script-src 'self' 'unsafe-inline' 'unsafe-eval'"
: "script-src 'self' 'unsafe-inline'";
? `script-src 'self' 'unsafe-inline' 'unsafe-eval' ${AHREFS_ORIGIN}`
: `script-src 'self' 'unsafe-inline' ${AHREFS_ORIGIN}`;
const CSP = [
"default-src 'self'",
SCRIPT_SRC,
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob:",
"font-src 'self' data:",
`connect-src 'self' ${RELAY_WS} ${VIDEO_RENDERER_ORIGIN}`,
`connect-src 'self' ${RELAY_WS} ${VIDEO_RENDERER_ORIGIN} ${AHREFS_ORIGIN}`,
`media-src 'self' ${VIDEO_RENDERER_ORIGIN}`,
"frame-ancestors 'none'",
"base-uri 'self'",
Expand Down
2 changes: 1 addition & 1 deletion src/app/chains/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export async function generateMetadata({
const url = `${SITE.url}/chains/${slug}`;
const title = `${chain.label} live benchmarks: finality, fees, RPC`;
const description = capDescription(
`${benches.length} live OpenChainBench measurements covering ${chain.label}. ${chain.description}`,
`${benches.length} live OpenChainBench measurement${benches.length === 1 ? "" : "s"} covering ${chain.label}. ${chain.description}`,
158,
);
return {
Expand Down
92 changes: 92 additions & 0 deletions src/app/feed.json/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* JSON Feed 1.1 (https://jsonfeed.org/version/1.1) mirror of /rss.xml.
*
* Modern feed readers + several AI agent tools (Perplexity's crawler,
* LangChain document loaders, MCP clients) prefer JSON Feed over RSS
* because JSON is trivial to parse without an XML dependency. RSS
* stays as the canonical feed for legacy aggregators; feed.json ships
* the same entries as a companion so tooling that only speaks JSON
* also picks up new benches without a custom scraper.
*
* Same freshness contract as rss.xml: pubDate per item = bench first
* commit; feed-level `date_modified` = generation time so aggregators
* see the feed as fresh whenever a headline number changes. Cache 5 min
* edge TTL, same as RSS.
*/

import { NextResponse } from "next/server";
import { AllBenchmarksDraftError, loadAllBenchmarks } from "@/lib/spec";
import { getBenchCreatedAt } from "@/lib/seo/bench-dates";
import { headlineSentence } from "@/lib/citation";
import { SITE } from "@/data/site";
import type { Benchmark } from "@/types/benchmark";

export const revalidate = 300;

function itemContentText(b: Benchmark): string {
const sentence = headlineSentence(b);
if (sentence) return `${sentence} ${b.subtitle}`.trim();
return b.subtitle;
}

export async function GET() {
let all;
try {
all = await loadAllBenchmarks();
} catch (err) {
if (err instanceof AllBenchmarksDraftError) {
return NextResponse.json(
{ error: "benchmarks_unavailable" },
{
status: 503,
headers: {
"cache-control": "no-store",
"retry-after": "60",
},
},
);
}
throw err;
}
const live = all.filter((b) => b.editorialStatus === "live");

const items = live
.map((b) => ({ bench: b, pubDate: getBenchCreatedAt(b.slug) }))
.sort((a, c) => c.pubDate.getTime() - a.pubDate.getTime())
.map(({ bench: b, pubDate }) => {
const url = `${SITE.url}/benchmarks/${b.slug}`;
return {
id: url,
url,
title: b.title,
content_text: itemContentText(b),
summary: b.subtitle,
date_published: pubDate.toISOString(),
...(b.lastRunAt ? { date_modified: b.lastRunAt } : {}),
tags: [b.category],
authors: [{ name: "OpenChainBench", url: SITE.url }],
};
});

const feed = {
version: "https://jsonfeed.org/version/1.1",
title: "OpenChainBench benchmark releases",
home_page_url: SITE.url,
feed_url: `${SITE.url}/feed.json`,
description:
"Live measurements for crypto infrastructure: RPCs, oracles, aggregators, bridges, prediction markets. One entry per public benchmark.",
language: "en",
icon: `${SITE.url}/logo.png`,
favicon: `${SITE.url}/icon`,
authors: [{ name: "OpenChainBench", url: SITE.url }],
date_modified: new Date().toISOString(),
items,
};

return NextResponse.json(feed, {
headers: {
"content-type": "application/feed+json; charset=utf-8",
"cache-control": "public, s-maxage=300, stale-while-revalidate=3600",
},
});
}
50 changes: 49 additions & 1 deletion src/app/hyperliquid/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export default async function HlFrontendPage({

const peers = pickPeers(history.frontends, frontend, 5);

const pageUrl = `https://openchainbench.com/hyperliquid/${slug}`;
const breadcrumbLd = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
Expand All @@ -141,7 +142,49 @@ export default async function HlFrontendPage({
"@type": "ListItem",
position: 3,
name: frontend.name,
item: `https://openchainbench.com/hyperliquid/${slug}`,
item: pageUrl,
},
],
};

// Content-shape schema. Previously only BreadcrumbList was emitted,
// which GSC treats as "no structured data for the primary content"
// on data-heavy pages (frontend revenue + volume timeseries). Add a
// Dataset node so Google Dataset Search + LLM crawlers index the
// 30-day revenue/volume figures as a citable measurement rather than
// treating the page as generic prose.
const datasetLd = {
"@context": "https://schema.org",
"@type": "Dataset",
"@id": `${pageUrl}#dataset`,
name: `${frontend.name} — HyperLiquid frontend revenue + volume`,
description: `Daily HyperLiquid frontend fees and volume for ${frontend.name}, sourced from the OpenChainBench hyperliquid-frontends benchmark. First measured ${firstDay}.`,
url: pageUrl,
identifier: slug,
creator: {
"@type": "Organization",
"@id": "https://openchainbench.com/#org",
name: "OpenChainBench",
url: "https://openchainbench.com",
},
publisher: {
"@type": "Organization",
"@id": "https://openchainbench.com/#org",
name: "OpenChainBench",
url: "https://openchainbench.com",
},
isAccessibleForFree: true,
license: "https://creativecommons.org/licenses/by/4.0/",
variableMeasured: [
"HyperLiquid frontend fees (30d)",
"HyperLiquid frontend volume (30d)",
],
temporalCoverage: `${new Date(firstDayMs).toISOString().slice(0, 10)}/..`,
distribution: [
{
"@type": "DataDownload",
encodingFormat: "application/json",
contentUrl: "https://openchainbench.com/api/stat/hyperliquid-frontends",
},
],
};
Expand All @@ -153,6 +196,11 @@ export default async function HlFrontendPage({
// biome-ignore lint/security/noDangerouslySetInnerHtml: serialized via safeJsonLd
dangerouslySetInnerHTML={{ __html: safeJsonLd(breadcrumbLd) }}
/>
<script
type="application/ld+json"
// biome-ignore lint/security/noDangerouslySetInnerHtml: serialized via safeJsonLd
dangerouslySetInnerHTML={{ __html: safeJsonLd(datasetLd) }}
/>

<Breadcrumb
items={[
Expand Down
9 changes: 9 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,15 @@ export default async function RootLayout({
title="OpenChainBench — new benchmarks"
href="/rss.xml"
/>
{/* JSON Feed 1.1 companion. Modern feed readers + AI agent
tooling (Perplexity, LangChain document loaders, MCP
clients) parse JSON without an XML dependency. */}
<link
rel="alternate"
type="application/feed+json"
title="OpenChainBench — new benchmarks (JSON Feed)"
href="/feed.json"
/>
<script
dangerouslySetInnerHTML={{
__html: `(function(){try{var t=localStorage.getItem('ocb-theme');var d=t==='dark'||(!t&&window.matchMedia('(prefers-color-scheme: dark)').matches);if(d)document.documentElement.classList.add('dark');}catch(e){}})();`,
Expand Down
2 changes: 1 addition & 1 deletion src/app/products/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export async function generateMetadata({
description,
alternates: { canonical: canonicalUrl },
openGraph: { title, description, type: "profile", url: canonicalUrl },
twitter: { card: "summary_large_image", title, description },
twitter: { card: "summary_large_image", site: SITE.twitter, title, description },
};
}

Expand Down
Loading