diff --git a/src/app/api/og/[slug]/route.tsx b/src/app/api/og/[slug]/route.tsx
index d2449071..3a8cae4f 100644
--- a/src/app/api/og/[slug]/route.tsx
+++ b/src/app/api/og/[slug]/route.tsx
@@ -129,7 +129,18 @@ export async function GET(
),
- size,
+ {
+ ...size,
+ // Every social/AI unfurl used to trigger a full satori render
+ // (max-age=0 → x-vercel-cache MISS on consecutive GETs). The
+ // underlying data moves every 60s but a 1h-old card is fine for
+ // an unfurl; s-maxage bounds renders to ~24/day/slug while SWR
+ // keeps scrapes instant.
+ headers: {
+ "cache-control":
+ "public, s-maxage=3600, stale-while-revalidate=86400",
+ },
+ },
);
}
diff --git a/src/app/benchmarks/[slug]/opengraph-image.tsx b/src/app/benchmarks/[slug]/opengraph-image.tsx
index 4b50596e..c46b72f7 100644
--- a/src/app/benchmarks/[slug]/opengraph-image.tsx
+++ b/src/app/benchmarks/[slug]/opengraph-image.tsx
@@ -153,6 +153,6 @@ export default async function OG({
),
- { ...size }
+ { ...size, headers: { "cache-control": "public, s-maxage=3600, stale-while-revalidate=86400" } }
);
}
diff --git a/src/app/benchmarks/[slug]/twitter-image.tsx b/src/app/benchmarks/[slug]/twitter-image.tsx
index f720ed0e..88fec66d 100644
--- a/src/app/benchmarks/[slug]/twitter-image.tsx
+++ b/src/app/benchmarks/[slug]/twitter-image.tsx
@@ -162,6 +162,6 @@ export default async function TwitterImage({
),
- { ...size }
+ { ...size, headers: { "cache-control": "public, s-maxage=3600, stale-while-revalidate=86400" } }
);
}
diff --git a/src/app/fee-compare/page.tsx b/src/app/fee-compare/page.tsx
index 3cc0f946..7baf3288 100644
--- a/src/app/fee-compare/page.tsx
+++ b/src/app/fee-compare/page.tsx
@@ -1,6 +1,8 @@
import type { Metadata } from "next";
import { pageMetadata } from "@/lib/page-metadata";
import { FeeCompareClient } from "@/components/fee-compare-client";
+import { buildBreadcrumbJsonLd, safeJsonLd } from "@/lib/jsonld";
+import { SITE } from "@/data/site";
export const metadata: Metadata = pageMetadata({
path: "/fee-compare",
@@ -26,8 +28,38 @@ export default async function FeeComparePage({
const rawDays = parseInt(params.days ?? "90", 10);
const initialDays = isFinite(rawDays) ? Math.min(180, Math.max(7, rawDays)) : 90;
+ // Only page on the site without JSON-LD until 2026-09: emit the same
+ // BreadcrumbList shape every hub page ships, plus a WebApplication
+ // node describing the comparison tool itself.
+ const jsonLd = {
+ "@context": "https://schema.org",
+ "@graph": [
+ buildBreadcrumbJsonLd([
+ { name: "Home", item: SITE.url },
+ { name: "Perp DEX fee comparison", item: `${SITE.url}/fee-compare` },
+ ]),
+ {
+ "@type": "WebApplication",
+ "@id": `${SITE.url}/fee-compare#app`,
+ name: "Perp DEX fee comparison",
+ url: `${SITE.url}/fee-compare`,
+ applicationCategory: "FinanceApplication",
+ operatingSystem: "Web",
+ offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
+ description:
+ "Compare taker fees between any two perp DEXs. Paste a wallet to see what was actually paid on Hyperliquid or Gains and what the same trades would have cost elsewhere.",
+ publisher: { "@id": `${SITE.url}/#org` },
+ },
+ ],
+ };
+
return (
+
Fee comparison
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 602973e5..dc6f4dff 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -10,6 +10,44 @@ import { safeJsonLd } from "@/lib/jsonld";
export const revalidate = 3600;
+/**
+ * Seed the live ticker with a real snapshot at ISR-regeneration time so
+ * the numbers exist in the cached HTML (most AI crawlers don't run JS
+ * and used to see dashes). Cost-neutral on Vercel: the fetch runs once
+ * per Data-Cache window (60s) shared across ALL visitors, never
+ * per-request — the page stays fully static/ISR. The client WebSocket
+ * takes over within seconds of hydration.
+ */
+async function fetchRelayStats(): Promise<
+ import("@/lib/live/types").GlobalView | null
+> {
+ try {
+ const res = await fetch("https://stream.openchainbench.com/stats", {
+ next: { revalidate: 60 },
+ signal: AbortSignal.timeout(3_000),
+ });
+ if (!res.ok) return null;
+ const d = (await res.json()) as { global?: Record };
+ const g = d.global;
+ if (!g || typeof g.vol24h !== "number") return null;
+ return {
+ vol24h: g.vol24h,
+ trades24h: g.trades24h ?? 0,
+ buys24h: g.buys24h ?? 0,
+ sells24h: g.sells24h ?? 0,
+ fees24h: g.fees24h ?? 0,
+ mcap: g.mcap ?? 0,
+ byChain: [],
+ lighthouseAt: g.lighthouseAt ?? 0,
+ mcapAt: g.mcapAt ?? 0,
+ };
+ } catch {
+ // Relay unreachable at regeneration time: render the pre-existing
+ // dashes rather than fail the page.
+ return null;
+ }
+}
+
const DESCRIPTION =
"Live benchmarks for crypto infrastructure: RPC latency, bridge fees, L2 finality and price feed accuracy. Open methodology, updated continuously.";
@@ -34,6 +72,7 @@ export const metadata: Metadata = {
export default async function HomePage() {
const benchmarks = await getBenchmarksSafe();
+ const initialStats = await fetchRelayStats();
// Same timestamp source the bench pages use for Dataset.dateModified:
// the harness lastRunAt (real data timestamp, not build time). The
@@ -104,7 +143,7 @@ export default async function HomePage() {
supported chains.
-
+
{/* Latest deployed benchmarks */}
diff --git a/src/app/reports/[category]/[slug]/opengraph-image.tsx b/src/app/reports/[category]/[slug]/opengraph-image.tsx
index 79ab3b05..9374af06 100644
--- a/src/app/reports/[category]/[slug]/opengraph-image.tsx
+++ b/src/app/reports/[category]/[slug]/opengraph-image.tsx
@@ -114,6 +114,6 @@ export default async function OG({
),
- { ...size }
+ { ...size, headers: { "cache-control": "public, s-maxage=3600, stale-while-revalidate=86400" } }
);
}
diff --git a/src/components/live/dashboard.tsx b/src/components/live/dashboard.tsx
index 07538a97..065ad2c1 100644
--- a/src/components/live/dashboard.tsx
+++ b/src/components/live/dashboard.tsx
@@ -108,8 +108,16 @@ function emptySeries(): SeriesByRange {
};
}
-export function LiveDashboard() {
- const [stats, setStats] = useState(null);
+export function LiveDashboard({
+ initialStats = null,
+}: {
+ /** Server-fetched snapshot of the relay's /stats global block, baked
+ * into the ISR HTML so crawlers (most AI bots don't run JS) see real
+ * ticker numbers instead of dashes. The WebSocket replaces it within
+ * seconds of hydration. */
+ initialStats?: GlobalView | null;
+} = {}) {
+ const [stats, setStats] = useState(initialStats);
const [recent, setRecent] = useState([]);
const [connected, setConnected] = useState(false);
const [series, setSeries] = useState(emptySeries);