A self-hosted GitHub stats card generator deployed on Cloudflare Workers (free tier). Generates dynamic SVG cards showing your GitHub stats and most-used languages, designed to be embedded in your GitHub profile README.
Inspired by github-readme-stats, rebuilt from scratch for Cloudflare Workers with zero external dependencies beyond Hono (router).
- Bun (used as package manager; Node/npm/yarn work too)
- A Cloudflare account (free tier is fine)
- A GitHub Personal Access Token (classic) — fine-grained tokens don't support the GraphQL API yet, so a classic PAT is required. Recommended scopes:
read:userandrepo(needed for private repo stats/languages)
# Install dependencies
bun install
# Set your GitHub token as a Cloudflare secret
bunx wrangler secret put GITHUB_TOKEN
# Paste your token when prompted
# Deploy
bunx wrangler deployYour worker will be live at https://github-readme.<your-subdomain>.workers.dev.
# Create a .dev.vars file with your token for local dev
echo 'GITHUB_TOKEN=ghp_your_token_here' > .dev.vars
# Start local dev server
bun run dev
# → http://localhost:8787.dev.vars is gitignored. Never commit tokens.
| Parameter | Description | Default |
|---|---|---|
username |
GitHub username (required) | — |
theme |
Card theme (see Themes) | default |
hide |
Comma-separated stats to hide: stars, commits, prs, issues, contribs |
— |
show_icons |
Show icons next to stats | true |
hide_border |
Hide card border | false |
hide_title |
Hide card title | false |
hide_rank |
Hide rank circle | false |
include_all_commits |
Count all lifetime commits (slower, extra API call) | false |
custom_title |
Override card title | — |
line_height |
Space between stat rows (px) | 25 |
border_radius |
Card corner radius (px) | 4.5 |
| Parameter | Description | Default |
|---|---|---|
username |
GitHub username (required) | — |
theme |
Card theme (see Themes) | default |
layout |
normal or compact |
normal |
langs_count |
Number of languages (1–20) | 5 |
exclude |
Comma-separated languages to exclude | — |
card_width |
Card width in pixels | 300 |
hide_border |
Hide card border | false |
hide_title |
Hide card title | false |
custom_title |
Override card title | — |
border_radius |
Card corner radius (px) | 4.5 |

default, dark, radical, tokyonight, dracula, gruvbox, catppuccin-mocha, nord, solarized, github-dark, rose_pine, onedark, cobalt, transparent
Themes are defined in src/common/themes.ts — add new ones by adding entries to the themes object.
src/
index.ts Hono router — endpoints: /, /api, /api/top-langs
common/
card.ts Base SVG card wrapper (title, border, background)
icons.ts GitHub octicon SVG paths
themes.ts Theme color definitions
utils.ts kFormatter, measureText, escapeHtml
cards/
stats-card.ts Stats card renderer (rank circle, stat rows)
top-langs-card.ts Top languages card (normal + compact layouts)
error-card.ts Error display card
fetchers/
stats.ts GitHub GraphQL/REST → user stats + rank calc
top-langs.ts GitHub GraphQL → language breakdown
wrangler.toml Cloudflare Workers config
- A request hits the worker (e.g.
/api?username=octocat) - The fetcher calls the GitHub GraphQL API (authenticated with your PAT)
- Data is passed to a card renderer that builds an SVG string
- The SVG is returned with
Cache-Controlheaders (4-hour TTL) so Cloudflare's CDN caches it
No database, no KV, no external services beyond the GitHub API.
Add a new theme: Add an entry in src/common/themes.ts and redeploy.
Add a new stat: Update the GraphQL query in src/fetchers/stats.ts, add the field to UserStats, and add a row in src/cards/stats-card.ts.
Add a new card type: Create a fetcher in src/fetchers/, a renderer in src/cards/, and a route in src/index.ts.
Change caching: The svgResponse() function in src/index.ts sets the Cache-Control max-age (default 14400s = 4 hours).
Rotate your PAT: Run bunx wrangler secret put GITHUB_TOKEN and paste the new token. No redeploy needed. Note: when creating your classic PAT, you can choose "No expiration" — the token is stored as an encrypted Cloudflare secret with read-only scopes, so the risk is minimal. If you prefer an expiring token, set a calendar reminder to rotate it before it expires or your cards will silently break.
- Cloudflare Workers free tier: 100,000 requests/day, 10ms CPU per invocation
- GitHub API: 5,000 requests/hour per PAT. With 4-hour CDN caching, this is more than enough for personal use.
- Bundle size: ~22KB gzipped (well under the 3MB limit)