diff --git a/sites/web/src/components/Header.astro b/sites/web/src/components/Header.astro
index fa1881e..da9dd98 100644
--- a/sites/web/src/components/Header.astro
+++ b/sites/web/src/components/Header.astro
@@ -1,7 +1,10 @@
---
+import { formatStarCount, getStarCount } from '~/lib/github'
import { MobileNav } from './MobileNav'
import { ThemeToggle } from './ThemeToggle'
+const starsLabel = formatStarCount(await getStarCount())
+
const navItems = [
{ href: '/why', label: 'Why' },
{ href: '/examples', label: 'Examples' },
@@ -79,32 +82,51 @@ const mobileLinks = [...navItems, referenceLink]
{referenceLink.label}
+ {/* Segmented "GitHub | count" on desktop (the familiar GitHub pattern);
+ collapses to a compact icon + count on mobile — no divider, no
+ second segment background. */}
-
- Star
+ {starsLabel}
+
diff --git a/sites/web/src/lib/github.ts b/sites/web/src/lib/github.ts
new file mode 100644
index 0000000..f3a20c3
--- /dev/null
+++ b/sites/web/src/lib/github.ts
@@ -0,0 +1,31 @@
+const REPO = 'desko27/react-call'
+
+// Used when the build-time fetch fails (offline CI, GitHub down, rate limit).
+// Roughly tracks the real count so the UI never shows an absurd number — it
+// only matters on the rare failed build, and refreshes on the next deploy.
+const FALLBACK_STARS = 1200
+
+// Astro renders Header on every page, so memoize: one network request per
+// build regardless of how many pages/components ask for the count.
+let cached: Promise | undefined
+
+export function getStarCount(): Promise {
+ if (!cached) {
+ cached = fetch(`https://api.github.com/repos/${REPO}`, {
+ headers: { Accept: 'application/vnd.github+json' },
+ })
+ .then((res) => {
+ if (!res.ok) throw new Error(`GitHub API ${res.status}`)
+ return res.json() as Promise<{ stargazers_count: number }>
+ })
+ .then((data) => data.stargazers_count)
+ .catch(() => FALLBACK_STARS)
+ }
+ return cached
+}
+
+// 1215 -> "1.2k", 980 -> "980", 12345 -> "12.3k". Trailing ".0" is dropped.
+export function formatStarCount(n: number): string {
+ if (n < 1000) return String(n)
+ return `${(n / 1000).toFixed(1).replace(/\.0$/, '')}k`
+}
diff --git a/sites/web/src/pages/index.astro b/sites/web/src/pages/index.astro
index 1d62eb3..8617fb8 100644
--- a/sites/web/src/pages/index.astro
+++ b/sites/web/src/pages/index.astro
@@ -6,10 +6,12 @@ import { HowItLives } from '~/components/landing/HowItLives'
import { MutationFlowSection } from '~/components/landing/MutationFlowSection'
import { NotJustConfirmations } from '~/components/landing/NotJustConfirmations'
import { StackSection } from '~/components/landing/StackSection'
+import { formatStarCount, getStarCount } from '~/lib/github'
import Base from '~/layouts/Base.astro'
import { getAllExamples } from '~/lib/examples'
const exampleCount = getAllExamples().length
+const starsLabel = formatStarCount(await getStarCount())
---
@@ -134,7 +136,7 @@ const onDelete = async () => {
href="https://github.com/desko27/react-call"
target="_blank"
rel="noopener noreferrer"
- aria-label="Star react-call on GitHub"
+ aria-label={`Star react-call on GitHub — ${starsLabel} stars`}
class="inline-flex items-center gap-2 rounded-md border border-[var(--color-border-strong)] bg-[var(--color-bg)] px-4 py-2 font-medium text-[var(--color-fg)] transition-colors hover:bg-[var(--color-bg-muted)]"
>
{
d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z"
>
- Star on GitHub
+ Star on GitHub
+ ·
+ {starsLabel}