feat(landing): auto-refresh live GitHub stars and image-pull stats (#292)

Fetch landing stars + image pulls at build time via a shared stats lib, refreshed by a daily cron + authenticated GITHUB_TOKEN. Image Pulls totals live Docker Hub pull_count + a maintained GHCR estimate (ghcr.io has no public pull-count API).
This commit is contained in:
SnapOtter
2026-06-22 14:11:48 +08:00
committed by GitHub
parent 9b1c105089
commit 33671d2d92
5 changed files with 141 additions and 56 deletions
+81
View File
@@ -0,0 +1,81 @@
// Shared, build-time stats for the landing page (GitHub stars + image pulls).
// The landing site ships zero client JS, so these are fetched in Astro
// frontmatter at build time and refreshed by a scheduled rebuild. Both
// fetchers degrade to a maintained constant if the upstream API is unreachable
// (or rate-limited), so a build never ships an empty number.
// ghcr.io exposes no public pull-count API, so the GitHub Container Registry
// portion is a manually maintained estimate. Update it as it grows.
const GHCR_ESTIMATE = 36_000;
// Fallbacks used when an upstream fetch fails. Keep roughly current so a
// degraded build still shows a believable figure.
const STAR_FALLBACK = 1720;
const DOCKER_FALLBACK = 104_000;
const GITHUB_REPO = "snapotter-hq/SnapOtter";
const DOCKERHUB_REPO = "snapotter/snapotter";
/** Compact integer formatting: 1720 -> "1.7k", 2_300_000 -> "2.3M". */
export function formatCompact(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(1).replace(/\.0$/, "")}k`;
return n.toString();
}
/**
* Image-pull formatting, rounded DOWN so the figure stays conservative:
* nearest 10K below 1M (140_801 -> "140K+"), nearest 0.1M at/above 1M
* (1_250_000 -> "1.2M+").
*/
export function formatPulls(total: number): string {
if (total >= 1_000_000) return `${Math.floor(total / 100_000) / 10}M+`;
return `${Math.floor(total / 10_000) * 10}K+`;
}
/**
* GitHub star count, fetched at build time. Sends an Authorization header when
* GITHUB_TOKEN is set (CI), lifting the unauthenticated 60 req/hr limit that
* otherwise pins the count to the fallback. Returns STAR_FALLBACK on failure.
*/
export async function getStarCount(): Promise<number> {
try {
const token = process.env.GITHUB_TOKEN;
const res = await fetch(`https://api.github.com/repos/${GITHUB_REPO}`, {
headers: {
Accept: "application/vnd.github+json",
"User-Agent": "SnapOtter-Landing",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
});
if (res.ok) {
const data = await res.json();
if (typeof data.stargazers_count === "number") return data.stargazers_count;
}
} catch {
// Network/JSON failure: fall through to the fallback below.
}
return STAR_FALLBACK;
}
/**
* Total image pulls = live Docker Hub pull_count + the GHCR estimate.
* Returns the raw total and a display string. Docker Hub degrades to
* DOCKER_FALLBACK if the API is unreachable.
*/
export async function getImagePulls(): Promise<{ total: number; display: string }> {
let dockerPulls = DOCKER_FALLBACK;
try {
const res = await fetch(`https://hub.docker.com/v2/repositories/${DOCKERHUB_REPO}/`);
if (res.ok) {
const data = await res.json();
if (typeof data.pull_count === "number" && data.pull_count > 0) {
dockerPulls = data.pull_count;
}
}
} catch {
// Network/JSON failure: keep the Docker fallback.
}
const total = dockerPulls + GHCR_ESTIMATE;
return { total, display: formatPulls(total) };
}