import type { LanguageShare } from "./languages"; const CARD_LANGUAGES = 6; /** The card's language strip: the largest languages as whole percents (at least 1), the rest as "Other". */ export function cardLanguages(languages: readonly LanguageShare[]): Array<{ name: string; percent: number }> { const total = languages.reduce((sum, language) => sum + language.bytes, 0); if (total === 0) return []; const shares = languages.map((language) => ({ name: language.name, bytes: language.bytes })); const kept = shares.filter((language) => language.name !== "Other").slice(0, CARD_LANGUAGES); const rest = total - kept.reduce((sum, language) => sum + language.bytes, 0); if (rest > 0) kept.push({ name: "Other", bytes: rest }); return kept.map((language) => ({ name: language.name, percent: Math.max(1, Math.round((language.bytes / total) * 100)), })); } /** Social card URLs and their version hashes; shared by page heads (client and server) and the /og handler. */ /** Bump when a card layout changes so every cached card URL changes with it. */ export const OG_RENDERER_VERSION = "2"; export const OG_WIDTH = 1200; export const OG_HEIGHT = 630; export const OG_PREFIX = "/og/"; export const SITE_OG_PATH = "/og/site.png"; export const SITE_DESCRIPTION = "gitfor hosts Git repositories with pull requests, Actions-style CI, issues, security scanning and sites. Open source under 0BSD, and you can run your own instance."; export const SITE_OG_ALT = "gitfor: open-source Git hosting, code review and CI"; /** FNV-1a 32-bit over the joined fields: cheap, sync and stable on both sides; it only needs to change, not resist collisions. */ export function ogVersion(fields: ReadonlyArray): string { const input = [ OG_RENDERER_VERSION, ...fields.map((field) => (field === null || field === undefined ? "" : String(field))), ].join("\u0000"); let hash = 0x811c9dc5; for (let index = 0; index < input.length; index++) { hash ^= input.charCodeAt(index); hash = Math.imul(hash, 0x01000193); } return (hash >>> 0).toString(16).padStart(8, "0"); } export type RepoCardData = { owner: string; name: string; description: string | null; stars: number; forks: number; openIssues: number; openPulls: number; avatarKey: string | null; latestRelease: string | null; topics: string[]; /** Whole-percent shares, so pushes that barely move the mix keep the card's URL. */ languages: Array<{ name: string; percent: number }>; }; export type AccountCardData = { name: string; kind: "user" | "org"; displayName: string | null; bio: string | null; avatarKey: string | null; publicRepos: number; }; export function repoCardVersion(repo: RepoCardData): string { return ogVersion([ "repo", repo.owner, repo.name, repo.description, repo.stars, repo.forks, repo.openIssues, repo.openPulls, repo.avatarKey, repo.latestRelease, repo.topics.join(","), repo.languages.map((language) => `${language.name}:${language.percent}`).join(","), ]); } export function accountCardVersion(account: AccountCardData): string { return ogVersion([ "account", account.name, account.kind, account.displayName, account.bio, account.avatarKey, account.publicRepos, ]); } export function featureCardVersion(slug: string, title: string, lede: string): string { return ogVersion(["feature", slug, title, lede]); } export function siteCardVersion(): string { return ogVersion(["site"]); } export function repoOgPath(repo: RepoCardData): string { return `/og/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.name)}.png?v=${repoCardVersion(repo)}`; } export function accountOgPath(account: AccountCardData): string { return `/og/${encodeURIComponent(account.name)}.png?v=${accountCardVersion(account)}`; } export function featureOgPath(slug: string, title: string, lede: string): string { return `/og/feature/${encodeURIComponent(slug)}.png?v=${featureCardVersion(slug, title, lede)}`; } export function siteOgPath(): string { return `${SITE_OG_PATH}?v=${siteCardVersion()}`; }