Skip to content

Commit 4d89e71

Browse files
Zubair Trabzadaclaude
andcommitted
Use self-hosted star chart image, star-history.com as click-through
GitHub now restricts stargazer data to repo owners/collaborators, so star-history.com's servers can no longer render this repo's chart (their embed times out with a 500 for everyone). Reinstate the in-repo daily-updated SVG as the README image and keep the star-history.com page as the link target. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent eadda64 commit 4d89e71

5 files changed

Lines changed: 185 additions & 1 deletion

File tree

.github/scripts/star_history.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#!/usr/bin/env python3
2+
"""Generate a self-hosted star-history chart for the README.
3+
4+
Fetches stargazer timestamps from the GitHub API (works with the built-in
5+
Actions GITHUB_TOKEN — no PAT needed) and renders assets/star-history.svg
6+
plus a dark-mode variant. Run by .github/workflows/star-history.yml daily.
7+
8+
Usage: GITHUB_TOKEN=<token> python3 .github/scripts/star_history.py
9+
"""
10+
11+
import datetime as dt
12+
import json
13+
import os
14+
import sys
15+
import urllib.request
16+
17+
REPO = os.environ.get("STAR_REPO", "zubair-trabzada/geo-seo-claude")
18+
TOKEN = os.environ.get("GITHUB_TOKEN", "")
19+
OUT_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "assets")
20+
PER_PAGE = 100
21+
MAX_POINTS = 240 # sampled points in the rendered path
22+
23+
24+
def fetch_page(page):
25+
req = urllib.request.Request(
26+
f"https://api.github.com/repos/{REPO}/stargazers?per_page={PER_PAGE}&page={page}",
27+
headers={
28+
"Accept": "application/vnd.github.star+json",
29+
"Authorization": f"Bearer {TOKEN}",
30+
"X-GitHub-Api-Version": "2022-11-28",
31+
"User-Agent": "star-history-generator",
32+
},
33+
)
34+
with urllib.request.urlopen(req, timeout=30) as resp:
35+
return json.load(resp)
36+
37+
38+
def fetch_star_dates():
39+
dates, page = [], 1
40+
while True:
41+
batch = fetch_page(page)
42+
if not batch:
43+
break
44+
dates.extend(
45+
dt.datetime.fromisoformat(s["starred_at"].replace("Z", "+00:00"))
46+
for s in batch
47+
if s.get("starred_at")
48+
)
49+
if len(batch) < PER_PAGE:
50+
break
51+
page += 1
52+
return sorted(dates)
53+
54+
55+
def sample(points, limit):
56+
if len(points) <= limit:
57+
return points
58+
step = (len(points) - 1) / (limit - 1)
59+
return [points[round(i * step)] for i in range(limit)]
60+
61+
62+
def render_svg(dates, fg, grid, accent, fill_opacity):
63+
w, h = 800, 420
64+
ml, mr, mt, mb = 70, 30, 50, 60
65+
pw, ph = w - ml - mr, h - mt - mb
66+
67+
total = len(dates)
68+
t0, t1 = dates[0], dates[-1]
69+
span = max((t1 - t0).total_seconds(), 1)
70+
71+
pts = [(i + 1, d) for i, d in enumerate(dates)]
72+
pts = sample(pts, MAX_POINTS)
73+
xy = [
74+
(ml + pw * (d - t0).total_seconds() / span, mt + ph * (1 - count / total))
75+
for count, d in pts
76+
]
77+
path = "M" + " L".join(f"{x:.1f},{y:.1f}" for x, y in xy)
78+
area = path + f" L{xy[-1][0]:.1f},{mt + ph} L{xy[0][0]:.1f},{mt + ph} Z"
79+
80+
y_ticks = 5
81+
y_labels = []
82+
for i in range(y_ticks + 1):
83+
val = round(total * i / y_ticks)
84+
y = mt + ph * (1 - i / y_ticks)
85+
y_labels.append(
86+
f'<line x1="{ml}" y1="{y:.1f}" x2="{ml + pw}" y2="{y:.1f}" stroke="{grid}" stroke-width="1"/>'
87+
f'<text x="{ml - 10}" y="{y + 4:.1f}" text-anchor="end" font-size="12" fill="{fg}" opacity="0.7">{val:,}</text>'
88+
)
89+
90+
x_labels = []
91+
for i in range(5):
92+
d = t0 + (t1 - t0) * i / 4
93+
x = ml + pw * i / 4
94+
anchor = "start" if i == 0 else "end" if i == 4 else "middle"
95+
x_labels.append(
96+
f'<text x="{x:.1f}" y="{mt + ph + 24}" text-anchor="{anchor}" font-size="12" fill="{fg}" opacity="0.7">{d.strftime("%b %d, %Y")}</text>'
97+
)
98+
99+
return f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif">
100+
<text x="{ml}" y="30" font-size="18" font-weight="600" fill="{fg}">Star History — {REPO}</text>
101+
<text x="{ml + pw}" y="30" text-anchor="end" font-size="14" fill="{accent}" font-weight="600">★ {total:,}</text>
102+
{''.join(y_labels)}
103+
<path d="{area}" fill="{accent}" opacity="{fill_opacity}"/>
104+
<path d="{path}" fill="none" stroke="{accent}" stroke-width="2.5" stroke-linejoin="round"/>
105+
{''.join(x_labels)}
106+
<text x="{w / 2}" y="{h - 8}" text-anchor="middle" font-size="11" fill="{fg}" opacity="0.5">Updated {dt.date.today().isoformat()} · generated in-repo, no external service</text>
107+
</svg>
108+
"""
109+
110+
111+
def main():
112+
if not TOKEN:
113+
sys.exit("GITHUB_TOKEN is not set")
114+
dates = fetch_star_dates()
115+
if not dates:
116+
sys.exit("No stargazer data returned")
117+
os.makedirs(OUT_DIR, exist_ok=True)
118+
variants = {
119+
"star-history.svg": ("#24292f", "#d0d7de", "#e3a008", "0.12"),
120+
"star-history-dark.svg": ("#e6edf3", "#30363d", "#e3b341", "0.15"),
121+
}
122+
for name, (fg, grid, accent, op) in variants.items():
123+
with open(os.path.join(OUT_DIR, name), "w") as f:
124+
f.write(render_svg(dates, fg, grid, accent, op))
125+
print(f"Rendered {len(variants)} SVGs from {len(dates):,} stars")
126+
127+
128+
if __name__ == "__main__":
129+
main()

.github/workflows/star-history.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Update star history chart
2+
3+
on:
4+
schedule:
5+
- cron: "17 6 * * *" # daily, 06:17 UTC
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
update-chart:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Generate SVGs
18+
env:
19+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20+
run: python3 .github/scripts/star_history.py
21+
22+
- name: Commit if changed
23+
run: |
24+
git config user.name "github-actions[bot]"
25+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
26+
git add assets/star-history.svg assets/star-history-dark.svg
27+
if git diff --cached --quiet; then
28+
echo "No changes"
29+
else
30+
git commit -m "chore: update star history chart"
31+
git push
32+
fi

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515

1616
## Star History
1717

18-
[![Star History Chart](https://api.star-history.com/svg?repos=zubair-trabzada/geo-seo-claude&type=Date)](https://www.star-history.com/#zubair-trabzada/geo-seo-claude&Date)
18+
<a href="https://www.star-history.com/#zubair-trabzada/geo-seo-claude&Date">
19+
<picture>
20+
<source media="(prefers-color-scheme: dark)" srcset="assets/star-history-dark.svg">
21+
<img alt="Star History Chart" src="assets/star-history.svg">
22+
</picture>
23+
</a>
1924

2025
---
2126

assets/star-history-dark.svg

Lines changed: 9 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)