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
+5 -17
View File
@@ -1,20 +1,8 @@
---
let starCount = "";
try {
const res = await fetch("https://api.github.com/repos/snapotter-hq/snapotter", {
headers: { "User-Agent": "SnapOtter-Landing" },
});
if (res.ok) {
const data = await res.json();
const count = data.stargazers_count;
if (typeof count === "number") {
starCount =
count >= 1000
? `${(count / 1000) % 1 === 0 ? (count / 1000).toFixed(0) : (count / 1000).toFixed(1)}k`
: count.toString();
}
}
} catch {}
import { formatCompact, getStarCount } from "@/lib/stats";
// Fetched at build time; refreshed by the scheduled landing rebuild.
const starCount = formatCompact(await getStarCount());
const links = [
{ label: "Features", href: "/#features" },
@@ -56,7 +44,7 @@ const links = [
>
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
<svg class="h-3.5 w-3.5 text-primary" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
<span id="gh-stars" class="text-xs">{starCount || "1.6k"}</span>
<span id="gh-stars" class="text-xs">{starCount}</span>
</a>
<a
href="https://demo.snapotter.com"
+5 -39
View File
@@ -1,43 +1,9 @@
---
function formatNumber(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();
}
import { formatCompact, getImagePulls, getStarCount } from "@/lib/stats";
let starCount = 0;
try {
const res = await fetch("https://api.github.com/repos/snapotter-hq/snapotter", {
headers: { Accept: "application/vnd.github.v3+json" },
});
if (res.ok) {
const data = await res.json();
starCount = data.stargazers_count ?? 0;
}
} catch {
starCount = 0;
}
// Docker Hub pull count (public API, refreshed at build time)
let dockerHubPulls = 0;
try {
const res = await fetch("https://hub.docker.com/v2/repositories/snapotter/snapotter/");
if (res.ok) {
const data = await res.json();
dockerHubPulls = data.pull_count ?? 0;
}
} catch {
dockerHubPulls = 0;
}
// GHCR (ghcr.io) has no public pull-count API, so this is a manual figure (update as it grows).
const GHCR_PULLS = 28_800;
const dockerForTotal = dockerHubPulls > 0 ? dockerHubPulls : 89_100;
const totalPulls = dockerForTotal + GHCR_PULLS;
const pullsDisplay =
totalPulls >= 1_000_000
? `${Math.floor(totalPulls / 100_000) / 10}M+`
: `${Math.floor(totalPulls / 100_000) * 100}K+`;
// Fetched at build time; refreshed by the scheduled landing rebuild.
const starCount = await getStarCount();
const { display: pullsDisplay } = await getImagePulls();
const stats = [
{
@@ -46,7 +12,7 @@ const stats = [
sublabel: "Across 5 file modalities",
},
{
value: starCount > 0 ? formatNumber(starCount) : "1.6k",
value: formatCompact(starCount),
label: "GitHub Stars",
sublabel: "Open-source & growing",
},
+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) };
}