diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..b925adae --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "mcp__openchainbench__list_benchmarks" + ] + } +} diff --git a/src/app/products/[slug]/page.tsx b/src/app/products/[slug]/page.tsx index eecaa93f..114eadd3 100644 --- a/src/app/products/[slug]/page.tsx +++ b/src/app/products/[slug]/page.tsx @@ -17,7 +17,7 @@ import { Breadcrumb } from "@/components/breadcrumb"; import { buildBreadcrumbJsonLd, safeJsonLd } from "@/lib/jsonld"; import { fetchHlBuilderStats, - isHlBuilderSlug, + isHlBuilderWithHistory, } from "@/lib/hl-builder-stats"; import { HlBuilderDashboard } from "@/components/hl-builder-dashboard"; import { RelatedProvidersSection } from "@/components/related-providers-section"; @@ -64,7 +64,7 @@ export async function generateMetadata({ // injection — return the canonical + redirect-safe metadata so // crawlers that peek at the response before the 308 fires still see the // right canonical target. - if (await isHlBuilderSlug(slug)) { + if (await isHlBuilderWithHistory(slug)) { const canonicalUrl = `${SITE.url}/hyperliquid/${slug}`; return { alternates: { canonical: canonicalUrl }, @@ -145,7 +145,7 @@ export default async function ProviderPage({ // frontends (12-month focus chart + peer group + KPI strip). Redirect // /products/ straight there so backlinks + old SERP entries // land on the richer hub without splitting rank signal across two URLs. - if (await isHlBuilderSlug(slug)) { + if (await isHlBuilderWithHistory(slug)) { redirect(`/hyperliquid/${slug}`); } const p = await getProvider(slug); @@ -158,7 +158,7 @@ export default async function ProviderPage({ // on-node harness has data for. Cheap (6 Prom scalars in parallel) and // gracefully degrades to a hidden section when Prom is unreachable or // the slug isn't an HL builder. - const isHlBuilder = await isHlBuilderSlug(p.slug); + const isHlBuilder = await isHlBuilderWithHistory(p.slug); const hlStats = isHlBuilder ? await fetchHlBuilderStats(p.slug) : null; // Prediction-market deep-dive: if the slug is a tracked PM venue or diff --git a/src/components/hl-cohort-leaderboard.tsx b/src/components/hl-cohort-leaderboard.tsx index 2a794b14..272f987f 100644 --- a/src/components/hl-cohort-leaderboard.tsx +++ b/src/components/hl-cohort-leaderboard.tsx @@ -114,6 +114,12 @@ export function HlCohortLeaderboard({ {filtered.map((r, i) => { const hist = historyBySlug?.get(r.slug); + // Only builders that have a `hist` entry actually have a + // detail page at /hyperliquid/ (the page component + // 404s when the history blob has no matching frontend, see + // hyperliquid/[slug]/page.tsx). Skip the link so Ahrefs + + // Bing don't queue up crawl paths that dead-end at 404. + const hasDetailPage = historyBySlug ? !!hist : true; return ( - - - - {r.name} + {hasDetailPage ? ( + + + + {r.name} + + + ) : ( + + + + {r.name} + - + )} {fmtUSD(r.revenue30d)} {fmtUSD(r.volume30d)} @@ -152,12 +167,16 @@ export function HlCohortLeaderboard({ )} - - Open → - + {hasDetailPage ? ( + + Open → + + ) : ( + + )} ); diff --git a/src/lib/hl-builder-stats.ts b/src/lib/hl-builder-stats.ts index dd793574..c4513369 100644 --- a/src/lib/hl-builder-stats.ts +++ b/src/lib/hl-builder-stats.ts @@ -70,6 +70,21 @@ export async function isHlBuilderSlug(slug: string): Promise { return cachedSlugs.has(slug); } +/** + * Same as `isHlBuilderSlug` but ALSO requires the slug to be present in + * the current history blob. Used before redirecting /products/ to + * /hyperliquid/ so we never send crawlers or users into a 404: a + * builder that's in the spec but not in the last-12-months history blob + * (dormant, new, or paused) would otherwise 404 at the redirect target. + * The 12-month history is the source of truth for what /hyperliquid/ + * can actually render (see hyperliquid/[slug]/page.tsx:83-85). + */ +export async function isHlBuilderWithHistory(slug: string): Promise { + if (!(await isHlBuilderSlug(slug))) return false; + const history = await fetchHlHistory(); + return !!history?.frontends.some((f) => f.slug === slug); +} + function promUrl(): string | null { return process.env.PROMETHEUS_URL?.trim() || null; }