Skip to content

Commit 8b847f6

Browse files
tianzhouclaude
andcommitted
Fetch GitHub stars client-side with build-time fallback
Star counts were baked in at build time and went stale until the next deploy. The badge now renders without a count in the static HTML, fetches the live count on mount, and falls back to the build-time number only if the client fetch fails — so the number appears once, without swapping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f777303 commit 8b847f6

2 files changed

Lines changed: 59 additions & 26 deletions

File tree

src/app/page.tsx

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Logo } from '@/components/logo'
2+
import { StarBadge } from '@/components/star-badge'
23

34
type Product = {
45
key: string
@@ -63,7 +64,8 @@ const products: Product[] = [
6364
},
6465
]
6566

66-
// Fetch star counts at build time (static export). Fails soft: on any error the
67+
// Fetch star counts at build time (static export), used only as a fallback
68+
// when the client-side fetch in StarBadge fails. Fails soft: on any error the
6769
// badge simply links to the repo without a count instead of breaking the build.
6870
async function getStars(repo: string): Promise<number | null> {
6971
try {
@@ -79,11 +81,6 @@ async function getStars(repo: string): Promise<number | null> {
7981
}
8082
}
8183

82-
function formatStars(n: number): string {
83-
if (n >= 1000) return `${(n / 1000).toFixed(1).replace(/\.0$/, '')}k`
84-
return String(n)
85-
}
86-
8784
// Conic blend of the four brand colours, rotated so each lands on its card's
8885
// corner: red (pgschema) top-right, orange (pgparser) bottom-right, green
8986
// (pgtui) bottom-left, blue (pgconsole) top-left; red repeats to close the loop.
@@ -157,26 +154,7 @@ export default async function Page() {
157154
alt=""
158155
className="h-auto w-20 shrink-0 transition-transform duration-200 group-hover:-translate-y-0.5 sm:w-28"
159156
/>
160-
<a
161-
href={`https://github.com/${prod.repo}`}
162-
target="_blank"
163-
rel="noreferrer"
164-
aria-label={`${prod.name} on GitHub${
165-
starCount !== null ? `, ${starCount} stars` : ''
166-
}`}
167-
className="pointer-events-auto relative z-10 inline-flex items-center gap-2 rounded-full border border-border px-3.5 py-1.5 text-sm font-medium tabular-nums text-ink-dim transition-colors hover:border-ink-dim hover:text-ink sm:px-4 sm:py-2 sm:text-base"
168-
>
169-
<svg
170-
width="18"
171-
height="18"
172-
viewBox="0 0 24 24"
173-
fill="currentColor"
174-
aria-hidden="true"
175-
>
176-
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
177-
</svg>
178-
{starCount !== null && <span>{formatStars(starCount)}</span>}
179-
</a>
157+
<StarBadge repo={prod.repo} name={prod.name} fallback={starCount} />
180158
</div>
181159

182160
{/* bottom: name + tagline + cta */}

src/components/star-badge.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use client'
2+
3+
import { useEffect, useState } from 'react'
4+
5+
function formatStars(n: number): string {
6+
if (n >= 1000) return `${(n / 1000).toFixed(1).replace(/\.0$/, '')}k`
7+
return String(n)
8+
}
9+
10+
// GitHub star badge. Renders without a count until the client-side fetch
11+
// settles, then shows the live count — or the build-time fallback if the
12+
// fetch fails (e.g. rate-limited). No count is rendered in the static HTML,
13+
// so the number never swaps in front of the user.
14+
export function StarBadge({
15+
repo,
16+
name,
17+
fallback,
18+
}: {
19+
repo: string
20+
name: string
21+
fallback: number | null
22+
}) {
23+
const [count, setCount] = useState<number | null>(null)
24+
25+
useEffect(() => {
26+
const controller = new AbortController()
27+
fetch(`https://api.github.com/repos/${repo}`, {
28+
headers: { Accept: 'application/vnd.github+json' },
29+
signal: controller.signal,
30+
})
31+
.then((res) => (res.ok ? res.json() : Promise.reject()))
32+
.then((data: { stargazers_count?: number }) => {
33+
setCount(typeof data.stargazers_count === 'number' ? data.stargazers_count : fallback)
34+
})
35+
.catch(() => {
36+
if (!controller.signal.aborted) setCount(fallback)
37+
})
38+
return () => controller.abort()
39+
}, [repo, fallback])
40+
41+
return (
42+
<a
43+
href={`https://github.com/${repo}`}
44+
target="_blank"
45+
rel="noreferrer"
46+
aria-label={`${name} on GitHub${count !== null ? `, ${count} stars` : ''}`}
47+
className="pointer-events-auto relative z-10 inline-flex items-center gap-2 rounded-full border border-border px-3.5 py-1.5 text-sm font-medium tabular-nums text-ink-dim transition-colors hover:border-ink-dim hover:text-ink sm:px-4 sm:py-2 sm:text-base"
48+
>
49+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
50+
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
51+
</svg>
52+
{count !== null && <span>{formatStars(count)}</span>}
53+
</a>
54+
)
55+
}

0 commit comments

Comments
 (0)