mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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) };
|
||||
}
|
||||
Reference in New Issue
Block a user