Files
zeekay 438b8644f3 rewards: /leaderboard and /bounty on the one points ledger
Every page that names points reads the cloud's rewards: the board
(GET /v1/rewards), the reader's standing, badges and history
(/v1/rewards/me), the listing (/v1/rewards/optin) and the rules table
(/v1/rewards/rules), snapshotted at build into
lib/data/rewards-rules.json and asked again when a page opens.  The
hard-coded RULES tiles, POINTS, the client-side score and
lib/hanzo/board.ts are gone; /beta and /referral draw the same Rules and
board and link to /leaderboard.

/leaderboard is the home: tabs by source and period, Your standing,
badges earned and locked, the rules, token milestones and levels, Link
GitHub through the existing per-person connection (the page completes
the grant GitHub returns), and Share your rank with a leaderboard card.
/bounty prints the contribution kinds and cap, three steps, the open
issues labelled bounty or good first issue in Hanzo's orgs (searched at
build with a token only), My contributions and the rules.  A star now
counts for the linked GitHub account, so Earn more offers Link GitHub.

The cards, nav, footer, llms.txt and sitemap carry both pages; /founder
no longer says points go nowhere.

Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
2026-10-27 01:56:45 -07:00

69 lines
2.4 KiB
TypeScript

import { TOPICS, page, picture } from '../hanzo/topics.ts'
/**
* The social preview cards: one per page that has its own, drawn by
* scripts/generate-og.mjs into public/og/, and the default card at
* public/og-image.png for every other page. Each is 2400x1260 (a 1.9:1 card at
* 2x), measured from the files.
*
* A share preview is cached by its address, so a redraw raises VERSION and
* every card takes a new one.
*/
const VERSION = 5
/** A card's address: its file in `public/`, at the cards' version. */
const card = (file: string) => `${file}?v=${VERSION}`
export const OG_IMAGE = {
url: card('/og-image.png'),
width: 2400,
height: 1260,
} as const
/** The pages with a card of their own. */
export const OG_IMAGES_BY_PATH: Record<string, string> = {
'/': card('/og/home.png'),
'/beta': card('/og/beta.png'),
'/referral': card('/og/beta.png'),
'/leaderboard': card('/og/leaderboard.png'),
'/bounty': card('/og/bounty.png'),
'/login': card('/og/login.png'),
'/signup': card('/og/signup.png'),
'/pricing': card('/og/pricing.png'),
'/models': card('/og/models.png'),
'/api': card('/og/api.png'),
'/enterprise': card('/og/enterprise.png'),
'/contact-sales': card('/og/enterprise.png'),
'/careers': card('/og/careers.png'),
'/research': card('/og/research.png'),
'/security': card('/og/security.png'),
'/products': card('/og/products.png'),
'/about': card('/og/about.png'),
// A share topic's page (app/beta/for/[topic]) unfurls to that topic's card;
// Hanzo's, the beta as a whole, is the beta page's own.
...Object.fromEntries(TOPICS.map((t) => [page(t.id), card(`/og/${picture(t.id)}.png`)])),
}
/** Resolves the OG card URL for a given path, defaulting to `/og-image.png`. */
export function getOgImageUrl(path?: string): string {
if (!path) return OG_IMAGE.url
const clean = path.replace(/\/+$/, '') || '/'
return OG_IMAGES_BY_PATH[clean] ?? OG_IMAGE.url
}
/** `openGraph.images` for a page — accepts an optional card URL override. */
export const ogImages = (alt: string, imageUrl?: string) => [
{
url: imageUrl ?? OG_IMAGE.url,
width: OG_IMAGE.width,
height: OG_IMAGE.height,
alt,
},
]
/** `twitter.images` — Twitter takes bare URLs. Defaults to the canonical card. */
export const twitterImages = [OG_IMAGE.url]
/** Helper to generate twitter.images with an explicit or path-resolved URL. */
export const getTwitterImages = (imageUrl?: string) => [imageUrl ?? OG_IMAGE.url]