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
61 changes: 42 additions & 19 deletions src/app/api/badge/[slug]/[provider]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,21 +220,14 @@ export async function GET(
// Legacy approximation for chain-dimensioned benches without a
// rank_matrix_query in their spec.
const scoped = rankOfChain(b, provider, chainParam);
if (!scoped) {
return new NextResponse("not found", {
status: 404,
// Short TTL: a scoped miss is usually transient (bench cache
// entry predating cellRanks, or a Prom hiccup on the matrix
// query), so don't let the CDN pin the 404 for long.
headers: { "cache-control": "public, s-maxage=60" },
});
}
// Serve a placeholder SVG instead of 404 on a transient scoped miss
// (bench cache entry predating cellRanks, Prom hiccup on the matrix
// query, provider without data on the scope). Keeps external embeds
// from displaying broken images and Ahrefs from flagging inbound.
if (!scoped) return placeholderSvg(b.title);
r = { rank: scoped.rank, total: scoped.total, value: scoped.value };
} else {
return new NextResponse("not found", {
status: 404,
headers: { "cache-control": "public, s-maxage=60" },
});
return placeholderSvg(b.title);
}
scopeLabel = [
chainParam ? chainLabel(b, chainParam) : null,
Expand All @@ -244,12 +237,7 @@ export async function GET(
.join(" · ");
} else {
r = rankOf(b.results, provider, b.higherIsBetter);
if (!r) {
return new NextResponse("not found", {
status: 404,
headers: { "cache-control": "public, s-maxage=60" },
});
}
if (!r) return placeholderSvg(b.title);
// Hint the aggregate scope when the bench declares dimensions so
// embedders can read it. Benches without dimensions get no scope
// label (it would be noise).
Expand Down Expand Up @@ -382,6 +370,41 @@ export async function GET(
});
}

/** Placeholder SVG served with HTTP 200 when the provider isn't ranked
* yet on this bench (missing p50, unknown scope cell, provider not in
* results). Serving 200 instead of 404 keeps embed images from breaking
* on partner sites during transient data gaps and keeps Ahrefs from
* flagging inbound links as broken. Short CDN TTL (60s) so a resolved
* gap surfaces quickly. */
function placeholderSvg(benchTitle: string): NextResponse {
const title = truncate(benchTitle, 60);
const line1 = "Measurement pending";
const line2 = "OpenChainBench";
const W = Math.min(
W_MAX,
Math.max(W_MIN, TEXT_X + title.length * CH_11 + 30),
);
const svg = `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" role="img" aria-label="OpenChainBench: ${escapeXml(title)} — measurement pending">
<title>OpenChainBench. ${escapeXml(title)}. Measurement pending.</title>
<rect width="${W}" height="${H}" rx="6" fill="#F5F1E8"/>
<rect x="0.5" y="0.5" width="${W - 1}" height="${H - 1}" rx="5.5" fill="none" stroke="#22272F" stroke-opacity="0.12"/>
<circle cx="24" cy="22" r="10" fill="none" stroke="#22272F" stroke-opacity="0.35" stroke-dasharray="2 2"/>
<text x="${TEXT_X}" y="18" font-family="SF Mono, Menlo, monospace" font-size="11" font-weight="600" fill="#22272F">${escapeXml(title)}</text>
<text x="${TEXT_X}" y="33" font-family="SF Mono, Menlo, monospace" font-size="10" fill="#6b7280">${escapeXml(line1)}</text>
<text x="${W - 8}" y="12" font-family="SF Mono, Menlo, monospace" font-size="7" fill="#22272F" fill-opacity="0.55" text-anchor="end">${escapeXml(line2)}</text>
</svg>`;
return new NextResponse(svg, {
status: 200,
headers: {
"Content-Type": "image/svg+xml; charset=utf-8",
Vary: "Accept, Accept-Encoding",
"Cache-Control":
"public, max-age=60, s-maxage=60, stale-while-revalidate=300",
},
});
}

// C0 control chars (minus \t \n \r) + DEL are forbidden in XML 1.0 text.
// A spec PR with a title containing one of these would otherwise produce
// an SVG that browsers refuse to render - self-DoS on every embed of the
Expand Down
138 changes: 130 additions & 8 deletions src/app/hyperliquid/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound, redirect } from "next/navigation";
import { notFound } from "next/navigation";
import {
fetchHlBuilderStats,
fetchHlCohort,
fetchHlHistory,
isHlBuilderSlug,
type HlCohortRow,
type HlHistoryFrontendCompact,
} from "@/lib/hl-builder-stats";
import { Breadcrumb } from "@/components/breadcrumb";
Expand Down Expand Up @@ -56,7 +57,18 @@ export async function generateMetadata({
const { slug } = await params;
const history = await fetchHlHistory();
const frontend = history?.frontends.find((f) => f.slug === slug);
if (!frontend) return {};
// Fallback title uses a title-cased slug so a tracked builder without
// a fresh history entry still ships a meaningful <title> (the page
// falls back to a placeholder render below when frontend is missing,
// matching this 200 metadata).
const displayName = frontend?.name ?? titleCaseSlug(slug);
if (!frontend) {
return pageMetadata({
path: `/hyperliquid/${slug}`,
title: `${displayName} — Hyperliquid frontend`,
description: `${displayName} on Hyperliquid: 12-month builder fees, volume and first-active date. Rolling 30-day metrics refresh as data becomes available.`,
});
}
const currentFees = lastNonNull(frontend.fees);
const peakFees = peakOf(frontend.fees);
const description = `${frontend.name} on Hyperliquid: ${fmtUSDShort(
Expand All @@ -66,7 +78,7 @@ export async function generateMetadata({
)}). 12-month history with volume, fees and first-active date.`;
return pageMetadata({
path: `/hyperliquid/${slug}`,
title: `${frontend.name} — Hyperliquid frontend`,
title: `${displayName} — Hyperliquid frontend`,
description,
});
}
Expand All @@ -85,13 +97,19 @@ export default async function HlFrontendPage({
// Data-outage guard: this page is the target of PERMANENT redirects
// from /products/<slug>, so it must never 404 on a runtime data gap
// (stale KV snapshot, Prom unreachable). Tracked builders without a
// resolvable history bounce temporarily (307) to the hub instead;
// crawlers keep the canonical URL and retry, users land somewhere
// useful. Unknown slugs still 404.
// resolvable history render a lightweight placeholder with HTTP 200
// instead of the previous 307 bounce to /hyperliquid. Ahrefs was
// flagging sitemap-listed builder URLs as 3xx-in-sitemap during the
// brief windows where the history blob dropped their entries; a 200
// placeholder keeps the URL crawlable while data catches up. Any
// partial data we still have (cohort row from the KPI grid) surfaces
// in the placeholder so the page isn't a soft 404. Unknown slugs
// still 404.
const frontend = history?.frontends.find((f) => f.slug === slug);
if (!history || !frontend) {
if (await isHlBuilderSlug(slug)) redirect("/hyperliquid");
notFound();
if (!(await isHlBuilderSlug(slug))) notFound();
const cohortRow = cohort?.rows.find((r) => r.slug === slug);
return <HlBuilderPending slug={slug} cohortRow={cohortRow} />;
}

const cohortRow = cohort?.rows.find((r) => r.slug === slug);
Expand Down Expand Up @@ -234,6 +252,110 @@ export default async function HlFrontendPage({
);
}

/** Title-case a slug ("mass-dot-money" → "Mass Dot Money"). Used as the
* display fallback for a tracked builder whose 12-month history entry
* hasn't materialised yet. */
function titleCaseSlug(slug: string): string {
return slug
.split("-")
.filter(Boolean)
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
.join(" ");
}

/** Lightweight placeholder rendered with HTTP 200 when a builder is in
* the spec catalog but temporarily absent from the history blob. Shows
* whatever cohort data is still resolvable so the page carries real
* content instead of a soft-404 shell. */
function HlBuilderPending({
slug,
cohortRow,
}: {
slug: string;
cohortRow: HlCohortRow | undefined;
}) {
const name = cohortRow?.name ?? titleCaseSlug(slug);
const breadcrumbLd = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Home",
item: "https://openchainbench.com/",
},
{
"@type": "ListItem",
position: 2,
name: "Hyperliquid",
item: "https://openchainbench.com/hyperliquid",
},
{
"@type": "ListItem",
position: 3,
name,
item: `https://openchainbench.com/hyperliquid/${slug}`,
},
],
};
return (
<article className="mx-auto max-w-[1200px] px-4 sm:px-6 py-12 sm:py-16">
<script
type="application/ld+json"
// biome-ignore lint/security/noDangerouslySetInnerHtml: serialized via safeJsonLd
dangerouslySetInnerHTML={{ __html: safeJsonLd(breadcrumbLd) }}
/>
<Breadcrumb
items={[
{ label: "Home", href: "/" },
{ label: "Hyperliquid", href: "/hyperliquid" },
{ label: name },
]}
/>
<header className="mb-8">
<p className="label-mono text-ink-faint mb-2">Hyperliquid frontend</p>
<div className="flex items-center gap-4">
<ProviderLogo slug={slug} name={name} size={56} />
<h1 className="display text-4xl sm:text-5xl text-ink">{name}</h1>
</div>
<p
className="mt-2 text-[12px] text-ink-faint"
style={{ fontFamily: "var(--font-mono, monospace)" }}
>
{slug}
</p>
</header>
{cohortRow ? (
<section className="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-10">
<Kpi label="Fees 30d" value={fmtUSDShort(cohortRow.revenue30d)} />
<Kpi label="Volume 30d" value={fmtUSDShort(cohortRow.volume30d)} />
<Kpi label="Users 30d" value={cohortRow.users30d.toLocaleString()} />
<Kpi
label="Cohort share 24h"
value={`${(cohortRow.cohortVolumeShare24h * 100).toFixed(2)}%`}
/>
</section>
) : null}
<section className="rounded-lg border border-ink/10 bg-paper-soft/40 p-5 max-w-2xl">
<h2 className="text-lg font-semibold text-ink mb-2">
12-month history aggregating
</h2>
<p className="text-sm text-ink-soft">
{name} is tracked on the Hyperliquid builder cohort. The full
12-month history chart, peer group and detailed KPIs will appear
here as soon as the next data sweep completes. In the meantime,
browse the{" "}
<Link href="/hyperliquid" className="underline hover:no-underline">
Hyperliquid frontends grid
</Link>{" "}
for live rankings.
</p>
</section>
</article>
);
}

function Kpi({ label, value }: { label: string; value: string }) {
return (
<div className="rounded-lg border border-ink/10 bg-paper p-4">
Expand Down
9 changes: 8 additions & 1 deletion src/lib/compare/adhoc-pairs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,15 @@ export function adHocPairs(profiles: ProviderProfile[]): AdHocPair[] {
const bBenches = liveBenchesBySlug.get(bSlug)!;
let sharedLive = 0;
for (const s of aBenches) if (bBenches.has(s)) sharedLive += 1;
// Buffer above the render-time noindex gate (< 2 live shared). The
// page recomputes on its own ISR clock, so a single bench flapping
// unavailable/back can drop `liveSharedCount` under 2 for one render
// window while the sitemap (force-dynamic) still lists the pair.
// Ahrefs then flags "noindex page in sitemap". Requiring >= 3 (brand)
// and >= 4 (otherwise) here means a single bench flap can't cross
// both thresholds simultaneously.
const bothBrand = BRAND_WHITELIST.has(aSlug) && BRAND_WHITELIST.has(bSlug);
const threshold = bothBrand ? 2 : 3;
const threshold = bothBrand ? 3 : 4;
if (sharedLive < threshold) continue;
out.push({ a: aSlug, b: bSlug, slug: `${aSlug}-vs-${bSlug}` });
}
Expand Down
Loading