Skip to content

Commit 6a0d9c9

Browse files
feat: track and display README badge embedders count
Adds a Redis-backed counter to the /api/badge route so we know how many distinct GitHub users have our graveyard SVG live in their READMEs. INCR for total renders (lower bound due to 5-minute edge cache), SADD on a 'stats:badge_usernames' set for unique-count. /api/stats exposes both as badgeRenders and badgeUsers. The homepage StatsCounter renders an optional third stat next to repos buried and profiles examined, hidden until the count is non-zero so a fresh deploy does not show an awkward 0000. Sitemap expanded to 13 sample /user URLs: the 4 confirmed badge embedders, the 3 curated-list maintainers who already link to us, and 8 famous-casualty orgs. Gives crawlers a richer set of graveyard pages to index alongside the static routes.
1 parent 979616f commit 6a0d9c9

5 files changed

Lines changed: 95 additions & 20 deletions

File tree

src/app/api/badge/route.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,32 @@ import { NextRequest, NextResponse } from 'next/server'
22

33
const VALID_USERNAME = /^[a-zA-Z0-9_.-]+$/
44

5+
async function getRedis() {
6+
if (!process.env.KV_REST_API_URL || !process.env.KV_REST_API_TOKEN) return null
7+
try {
8+
const { Redis } = await import('@upstash/redis')
9+
return new Redis({ url: process.env.KV_REST_API_URL, token: process.env.KV_REST_API_TOKEN })
10+
} catch {
11+
return null
12+
}
13+
}
14+
15+
// Tracks badge usage without slowing the response: total renders go up
16+
// every request, and the username is added to a set so we can count unique
17+
// graveyards being tracked across the wild.
18+
async function trackBadgeRender(username: string) {
19+
try {
20+
const redis = await getRedis()
21+
if (!redis) return
22+
await Promise.all([
23+
redis.incr('stats:badge_renders'),
24+
redis.sadd('stats:badge_usernames', username.toLowerCase()),
25+
])
26+
} catch {
27+
// never block the SVG response on telemetry
28+
}
29+
}
30+
531
interface GHRepo {
632
pushed_at: string | null
733
archived: boolean
@@ -63,12 +89,12 @@ function buildSvg(
6389
<text x="16" y="28" font-family=${JSON.stringify(MONO)} font-size="9" font-weight="700" fill="#9a9288" letter-spacing="2.2">GITHUB REPO GRAVEYARD</text>
6490
6591
<text x="16" y="58" font-family=${JSON.stringify(MONO)} font-size="15" font-weight="700" letter-spacing="0.8">
66-
<tspan fill="#8B0000">${dead} DEAD</tspan><tspan fill="#cec6bb" font-weight="400"> · </tspan><tspan fill="${struggling > 0 ? '#b45309' : '#cec6bb'}" font-weight="${struggling > 0 ? '700' : '400'}">${struggling} STRUGGLING</tspan><tspan fill="#cec6bb" font-weight="400"> · </tspan><tspan fill="${alive > 0 ? '#2d7a3c' : '#cec6bb'}" font-weight="${alive > 0 ? '700' : '400'}">${alive} ALIVE</tspan>
92+
<tspan fill="#8B1A1A">${dead} DEAD</tspan><tspan fill="#cec6bb" font-weight="400"> · </tspan><tspan fill="${struggling > 0 ? '#b45309' : '#cec6bb'}" font-weight="${struggling > 0 ? '700' : '400'}">${struggling} STRUGGLING</tspan><tspan fill="#cec6bb" font-weight="400"> · </tspan><tspan fill="${alive > 0 ? '#2d7a3c' : '#cec6bb'}" font-weight="${alive > 0 ? '700' : '400'}">${alive} ALIVE</tspan>
6793
</text>
6894
6995
<rect x="${BAR_X}" y="${BAR_Y}" width="${BAR_W}" height="${BAR_H}" fill="#e0d8ce"/>
7096
<g clip-path="url(#bar-clip)">
71-
${deadW > 0 ? `<rect x="${BAR_X}" y="${BAR_Y}" width="${deadW}" height="${BAR_H}" fill="#8B0000"/>` : ''}
97+
${deadW > 0 ? `<rect x="${BAR_X}" y="${BAR_Y}" width="${deadW}" height="${BAR_H}" fill="#8B1A1A"/>` : ''}
7298
${strugglingW > 0 ? `<rect x="${BAR_X + deadW}" y="${BAR_Y}" width="${strugglingW}" height="${BAR_H}" fill="#b45309"/>` : ''}
7399
${aliveW > 0 ? `<rect x="${BAR_X + deadW + strugglingW}" y="${BAR_Y}" width="${aliveW}" height="${BAR_H}" fill="#2d7a3c"/>` : ''}
74100
</g>
@@ -87,6 +113,10 @@ export async function GET(req: NextRequest) {
87113
const stats = await fetchStats(username)
88114
const { dead, struggling, alive, total } = stats ?? { dead: 0, struggling: 0, alive: 0, total: 0 }
89115

116+
// Fire-and-forget telemetry. Edge caching means we only see this on cache
117+
// miss, so the count is a lower bound, not raw view count.
118+
void trackBadgeRender(username)
119+
90120
const svg = buildSvg(username, dead, struggling, alive, total, framed)
91121

92122
return new NextResponse(svg, {

src/app/api/stats/route.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,25 @@ async function getRedis() {
2121
export async function GET() {
2222
try {
2323
const redis = await getRedis()
24-
if (!redis) return NextResponse.json({ buried: BURIED_HISTORICAL_BASELINE, shared: 0, downloaded: 0, profiles: 0 })
25-
const [buried, shared, downloaded, profiles] = await Promise.all([
24+
if (!redis) return NextResponse.json({ buried: BURIED_HISTORICAL_BASELINE, shared: 0, downloaded: 0, profiles: 0, badgeRenders: 0, badgeUsers: 0 })
25+
const [buried, shared, downloaded, profiles, badgeRenders, badgeUsers] = await Promise.all([
2626
redis.get<number>('stats:buried'),
2727
redis.get<number>('stats:shared'),
2828
redis.get<number>('stats:downloaded'),
2929
redis.get<number>('stats:profiles'),
30+
redis.get<number>('stats:badge_renders'),
31+
redis.scard('stats:badge_usernames'),
3032
])
3133
return NextResponse.json({
32-
buried: normalizeBuriedCount(buried),
33-
shared: shared ?? 0,
34-
downloaded: downloaded ?? 0,
35-
profiles: profiles ?? 0,
34+
buried: normalizeBuriedCount(buried),
35+
shared: shared ?? 0,
36+
downloaded: downloaded ?? 0,
37+
profiles: profiles ?? 0,
38+
badgeRenders: badgeRenders ?? 0,
39+
badgeUsers: badgeUsers ?? 0,
3640
})
3741
} catch {
38-
return NextResponse.json({ buried: BURIED_HISTORICAL_BASELINE, shared: 0, downloaded: 0 })
42+
return NextResponse.json({ buried: BURIED_HISTORICAL_BASELINE, shared: 0, downloaded: 0, badgeRenders: 0, badgeUsers: 0 })
3943
}
4044
}
4145

src/app/page.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ function HomePage() {
5353
const [displayedBuried, setDisplayedBuried] = useState<number | null>(null)
5454
const [profiles, setProfiles] = useState<number | null>(null)
5555
const [displayedProfiles, setDisplayedProfiles] = useState<number | null>(null)
56+
const [badgeUsers, setBadgeUsers] = useState<number | null>(null)
5657
const [statsLoading, setStatsLoading] = useState(true)
5758
const lastHandledRepoRef = useRef<string | null>(null)
5859

@@ -88,9 +89,10 @@ function HomePage() {
8889
useEffect(() => {
8990
fetch('/api/stats')
9091
.then(r => r.json())
91-
.then((d: { buried: number; profiles: number }) => {
92+
.then((d: { buried: number; profiles: number; badgeUsers?: number }) => {
9293
setBuried(d.buried ?? 0)
9394
setProfiles(d.profiles ?? 0)
95+
setBadgeUsers(d.badgeUsers ?? 0)
9496
})
9597
.catch(() => {})
9698
.finally(() => setStatsLoading(false))
@@ -150,7 +152,7 @@ function HomePage() {
150152
<div style={{ marginTop: '0px' }}>
151153
<PageHero
152154
subtitle={
153-
!statsLoading ? <StatsCounter buried={displayedBuried ?? 0} profiles={displayedProfiles ?? 0} /> : <span style={{ minHeight: '24px', display: 'block' }} />
155+
!statsLoading ? <StatsCounter buried={displayedBuried ?? 0} profiles={displayedProfiles ?? 0} badgeUsers={badgeUsers ?? 0} /> : <span style={{ minHeight: '24px', display: 'block' }} />
154156
}
155157
microcopy={null}
156158
/>
@@ -205,8 +207,8 @@ function HomePage() {
205207
{/* User scan error */}
206208
{userFetchError && !userLoading && (
207209
<div style={{ textAlign: 'center', padding: '40px 0' }}>
208-
<p style={{ fontFamily: MONO, fontSize: '13px', color: '#8B0000', marginBottom: '20px' }}>{userFetchError}</p>
209-
<button className="alive-interactive" onClick={resetUser} style={{ fontFamily: MONO, fontSize: '13px', fontWeight: 700, background: 'none', border: 'none', textDecoration: 'underline', textUnderlineOffset: '3px', color: '#160A06', cursor: 'pointer' }}>
210+
<p style={{ fontFamily: MONO, fontSize: '13px', color: 'var(--c-red)', marginBottom: '20px' }}>{userFetchError}</p>
211+
<button className="alive-interactive" onClick={resetUser} style={{ fontFamily: MONO, fontSize: '13px', fontWeight: 700, background: 'none', border: 'none', textDecoration: 'underline', textUnderlineOffset: '3px', color: 'var(--c-ink)', cursor: 'pointer' }}>
210212
← examine another subject
211213
</button>
212214
</div>
@@ -226,8 +228,8 @@ function HomePage() {
226228
letterSpacing: '0.06em',
227229
transition: 'color 0.15s',
228230
}}
229-
onMouseEnter={e => { (e.currentTarget as HTMLButtonElement).style.color = '#1a1a1a' }}
230-
onMouseLeave={e => { (e.currentTarget as HTMLButtonElement).style.color = '#9a9288' }}
231+
onMouseEnter={e => { (e.currentTarget as HTMLButtonElement).style.color = 'var(--c-ink)' }}
232+
onMouseLeave={e => { (e.currentTarget as HTMLButtonElement).style.color = 'var(--c-muted)' }}
231233
>
232234
← back
233235
</button>

src/app/sitemap.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,37 @@ import { MetadataRoute } from 'next'
22

33
const BASE_URL = 'https://commitmentissues.dev'
44

5-
// Sample user-graveyard URLs to expose the dynamic /user/[name] route shape
6-
// to crawlers. These pages have their own metadata (per-user title/description).
5+
// User-graveyard URLs to expose the dynamic /user/[name] route shape to
6+
// crawlers. These pages have their own metadata (per-user title/description).
7+
// Mix of (a) developers whose READMEs already embed our badge — Google
8+
// notices the cross-link — and (b) well-known orgs whose archive status
9+
// makes their graveyard page interesting on its own.
710
const SAMPLE_USERS = [
11+
// Our own org
812
'dotsystemsdevs',
13+
// Confirmed badge embedders (verified via gh code search 2026-05-22)
14+
'JohanSanSebastian',
15+
'adiz777',
16+
'lord-vinayak',
17+
'jaritrix02',
18+
// Curated-list maintainers who already link to us
19+
'pegaltier',
20+
'YamilAyma',
21+
'Diego2005z',
22+
// Famous casualty orgs (from Hall of Shame)
923
'atom',
1024
'angular',
1125
'apache',
1226
'facebookarchive',
1327
'YahooArchive',
28+
'gulpjs',
29+
'gruntjs',
30+
'bower',
31+
'mootools',
32+
'knockout',
33+
'jashkenas',
34+
'ariya',
35+
'meteor',
1436
]
1537

1638
export default function sitemap(): MetadataRoute.Sitemap {

src/components/StatsCounter.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ const PersonIcon = () => (
1414
</svg>
1515
)
1616

17+
const BadgeIcon = () => (
18+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden>
19+
<path d="M3.75 1a.75.75 0 0 0-.75.75v12.5c0 .414.336.75.75.75h8.5a.75.75 0 0 0 .75-.75V1.75a.75.75 0 0 0-.75-.75h-8.5ZM5 4h6v1.5H5V4Zm0 3h6v1.5H5V7Zm0 3h4v1.5H5V10Z"/>
20+
</svg>
21+
)
22+
1723
const DIGIT_WIDTH = 18
1824
const DIGIT_HEIGHT = 24
1925
const MIN_DIGITS = 4
@@ -56,9 +62,10 @@ function Counter({ value }: { value: number }) {
5662
interface Props {
5763
buried: number
5864
profiles: number
65+
badgeUsers?: number
5966
}
6067

61-
export default function StatsCounter({ buried, profiles }: Props) {
68+
export default function StatsCounter({ buried, profiles, badgeUsers }: Props) {
6269
return (
6370
<div style={{
6471
display: 'inline-flex',
@@ -69,17 +76,27 @@ export default function StatsCounter({ buried, profiles }: Props) {
6976
fontFamily: MONO,
7077
minHeight: '24px',
7178
}}>
72-
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px', color: 'var(--c-ink)' }}>
79+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px', color: 'var(--c-ink)' }} title="Repos buried">
7380
<RepoIcon />
7481
<Counter value={buried} />
7582
</span>
7683

7784
<span aria-hidden style={{ color: 'var(--c-muted)', fontSize: '10px' }}></span>
7885

79-
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px', color: 'var(--c-ink)' }}>
86+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px', color: 'var(--c-ink)' }} title="Profiles examined">
8087
<PersonIcon />
8188
<Counter value={profiles} />
8289
</span>
90+
91+
{badgeUsers !== undefined && badgeUsers > 0 && (
92+
<>
93+
<span aria-hidden style={{ color: 'var(--c-muted)', fontSize: '10px' }}></span>
94+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px', color: 'var(--c-ink)' }} title="Graveyards embedded as README badges">
95+
<BadgeIcon />
96+
<Counter value={badgeUsers} />
97+
</span>
98+
</>
99+
)}
83100
</div>
84101
)
85102
}

0 commit comments

Comments
 (0)