A self-hosted GitHub README stats card generator. Single codebase (Node.js/Express or Cloudflare Workers) that is a drop-in replacement for:
anuraghazra/github-readme-stats(stats, top languages, repo pin)DenverCoder1/github-readme-streak-stats(streak stats)DenverCoder1/readme-typing-svg(typing SVG)DenverCoder1/custom-icon-badges(badges with custom icons)DenverCoder1/github-readme-youtube-cards(YouTube cards)- shields.io dynamic JSON badges
📚 EXAMPLES.md — a cookbook of every endpoint and variation as live, copy-pasteable URLs.
Once deployed, embed any card in your GitHub README as a standard Markdown image:





Endpoint: GET /api/stats
Displays your GitHub stats: total stars, commits, PRs, issues, followers, contributed-to count, and an animated rank badge.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
username |
string | required | GitHub username |
show_icons |
boolean | false |
Show icons next to each stat |
hide |
string | — | Comma-separated stats to hide: stars, commits, prs, issues, followers, contribs |
hide_rank |
boolean | false |
Hide the rank badge |
ring_color |
hex | title color | Color of the rank ring (without #) |
include_all_commits |
boolean | false |
Count all-time commits instead of just the current year |
number_format |
short | long |
short |
short → 1.2k / long → 1,234 |
cache_seconds |
number | 21600 |
Cache TTL in seconds (6 hours default) |
Examples:
<!-- Dark theme with icons, hiding issues -->

<!-- Custom ring color, all-time commits, long number format -->

<!-- Minimal — no rank badge, no title -->
Endpoint: GET /api/top-langs
Shows the programming languages you use most across your public repositories, measured by bytes of code.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
username |
string | required | GitHub username |
layout |
normal | compact | donut | pie |
normal |
Card layout style |
langs_count |
number | 5 |
Number of languages to show (max from your repos) |
hide |
string | — | Comma-separated language names to exclude, e.g. html,css |
exclude_repo |
string | — | Comma-separated repo names to exclude from language counting |
hide_progress |
boolean | false |
Hide the progress bars (normal layout only) |
cache_seconds |
number | 86400 |
Cache TTL in seconds (24 hours default) |
Layout styles:
<!-- Normal: stacked list with progress bars (default) -->

<!-- Compact: two-column grid -->

<!-- Donut chart -->

<!-- Pie chart -->
Examples:
<!-- Show 8 languages, hide HTML and CSS, compact layout -->

<!-- Exclude a repo from counting, tokyonight theme -->

<!-- Donut chart without a title -->
Endpoint: GET /api/pin
Shows a card for a specific repository with its description, primary language, star count, and fork count.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
username |
string | required | Repository owner's GitHub username |
repo |
string | required | Repository name |
show_owner |
boolean | false |
Show owner/repo instead of just repo in the title |
cache_seconds |
number | 86400 |
Cache TTL in seconds (24 hours default) |
Examples:
<!-- Basic repo pin -->

<!-- Show owner prefix, radical theme -->
Endpoint: GET /api/streak
A native reimplementation of DenverCoder1/github-readme-streak-stats inside this repo — no PHP, no second service. Shows your total contributions, current streak, and longest streak (with date ranges) in the classic three-column layout with the flame-in-a-ring centerpiece.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
username |
string | required | GitHub username. user= is accepted as an alias (upstream compatibility) |
mode |
daily | weekly |
daily |
weekly counts consecutive Sun–Sat weeks with ≥1 contribution |
exclude_days |
string | — | Comma-separated days to skip: Sun,Mon,Tue,Wed,Thu,Fri,Sat. Excluded days never break a streak and their contributions aren't counted |
exclude_dates |
string | — | Dates to skip: 2026-01-01, ranges 2026-03-01..2026-03-15, or annual 12-25 (every year) |
date_format |
string | M j[, Y] |
PHP-style format for the date ranges: d j D l S n m M F Y y. Text in [brackets] is only shown when the date's year ≠ current year |
timezone |
string | UTC |
IANA zone (Asia/Manila) or fixed offset (+08:00, -5). Determines which day counts as "today" for the current streak |
starting_year |
number | account creation | First year to scan for the longest streak / totals (max 25 years back) |
type |
svg | json |
svg |
json returns the raw streak data instead of an image |
locale |
string | — | Locale for date rendering when date_format is absent, e.g. de, fil-PH |
card_width |
number | 495 |
Card width in px (clamped to 320–800) |
disable_animations |
boolean | false |
Turn off the fade-in animation |
hide_title |
boolean | true |
Inverted vs other cards — the streak card has no title by default (matches upstream). Pass hide_title=false to show one |
custom_title |
string | <user> GitHub Streak |
Title text when shown |
theme, hide_border, border_radius, bg_color, border_color, text_color, title_color |
— | — | All Common Options work |
background, border, stroke, ring, fire, currStreakNum, currStreakLabel, sideNums, sideLabels, dates |
hex | theme | Upstream color params (with or without #) — see migration |
The card is cached for 6 hours per unique URL by default (override with the CACHE_SECONDS env var) so the current streak stays reasonably fresh without hammering the GitHub API.
Examples:
<!-- Basic streak card -->

<!-- Dark theme, weekends excluded -->

<!-- Weekly mode, Philippine timezone -->

<!-- Skip a vacation + annual holidays, long date format -->

<!-- Upstream-style explicit colors: fire, ring, and per-section text -->

<!-- Raw JSON data for your own tooling -->
curl "https://gh-stats.skiddph.com/api/streak?username=eru123&type=json"{
"totalContributions": 14404,
"currentStreak": { "length": 79, "start": "2026-06-08", "end": "2026-08-25" },
"longestStreak": { "length": 85, "start": "2022-01-18", "end": "2022-04-12" },
"startingYear": 2018,
"mode": "daily"
}- Data source — the card reads GitHub's official GraphQL API (
contributionsCollection → contributionCalendar), the same calendar GitHub renders on your profile. Two requests per uncached render: one to get your account's creation year, then one batched query with a per-year alias from that year (orstarting_year) to the current year — so even a 10-year account is a single round trip. It uses the sameGITHUB_TOKENas every other card (read:userscope is enough) and respectsWHITELIST. - Streak math — days are walked in UTC day buckets. A day with ≥1 contribution extends a run; an empty day ends it. Two conveniences: today doesn't count against you (the day isn't over — an empty today falls back to yesterday before checking), and excluded days are transparent (they're skipped entirely, never breaking a run, whether or not you contributed that day).
mode=weeklybuckets days into Sun–Sat weeks first, then applies the same logic to weeks. Excluded days'/dates' contributions are subtracted from the total. - Rendering — pure string-built SVG, no DOM/rasterizer, so it runs identically on Node.js and Cloudflare Workers (no Node-only APIs — this is why it deploys to the same Worker as the rest of gh-stats). It shares the theme engine with all other cards, so
theme=tokyonightlooks consistent across your stats, langs, pin, and streak cards. - Caching — responses are cached per full URL (6 h default): in the Cloudflare Cache API on Workers, in memory on Node. GitHub itself is only hit on cache misses, and the browser gets
Cache-Control: public, max-ageheaders too.
Swap the host and path, keep the rest of the URL — user= works as-is:
<!-- before -->

<!-- after -->
Option compatibility at a glance:
| Upstream option | Status here |
|---|---|
user |
✅ accepted as-is (alias of username) |
date_format (incl. [brackets]) |
✅ same PHP-style tokens: d j D l S n m M F Y y |
mode=daily|weekly, exclude_days, exclude_dates (incl. ranges & annual dates) |
✅ supported |
timezone, card_width, border_radius, hide_border, disable_animations |
✅ supported |
background, border, stroke, ring, fire, currStreakNum, currStreakLabel, sideNums, sideLabels, dates |
✅ supported, same meanings |
theme |
default, dark, radical, tokyonight, dracula, gruvbox, onedark, transparent (shared with all cards). Recreate any upstream theme exactly with the color params above |
locale |
de, fil-PH); card labels remain English |
type=png |
❌ not supported (no rasterizer — SVG keeps the service dependency-free). type=json is supported |
exclude_days_label and other label overrides |
❌ labels are fixed English: Total Contributions / Current Streak / Longest Streak |
Endpoint: GET /api/typing
A native reimplementation of DenverCoder1/readme-typing-svg. Animates text typing itself out line by line in a README-safe SVG. No token required — fully standalone.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
lines |
string | required | Semicolon-separated lines to type (; separator) |
separator |
string | ; |
Change the separator if your lines contain semicolons |
font |
string | monospace |
Font family — falls back to monospace inside GitHub's image proxy |
size |
number | 20 |
Font size in px |
color |
hex | 36BCF7 |
Text color |
background |
hex | 00000000 |
Background (transparent by default) |
width |
number | 400 |
SVG width — increase for long lines |
height |
number | computed | SVG height |
center |
boolean | false |
Horizontally center the text |
vCenter |
boolean | false |
Vertically center the text |
multiline |
boolean | false |
Keep previous lines on screen while the next types |
duration |
number | 5000 |
Milliseconds to type one line |
pause |
number | 0 |
Milliseconds to hold a finished line before erasing |
repeat |
boolean | true |
Loop forever (false plays once and freezes on the last line) |
letterSpacing |
string | normal |
CSS letter-spacing |
Migration from readme-typing-svg: swap the host, keep everything after ?:
<!-- before -->

<!-- after -->
All documented upstream parameters are supported. Literal dashes work the same way as upstream — encode as -- if needed.
Endpoints: GET /api/badge/...
A reimplementation of DenverCoder1/custom-icon-badges plus a native shields.io-compatible badge renderer and dynamic badge formatter.

Path format: /api/badge/<label>-<message>-<color> — encode a literal - inside label/message as --. Colors accept shields named colors (brightgreen, blue, critical, …) or hex.
Fetches any JSON URL and renders the queried value as a badge — native replacement for shields' /badge/dynamic/json:
Parameters (badges):
| Parameter | Type | Default | Description |
|---|---|---|---|
style |
flat | plastic | flat-square | for-the-badge | social |
flat |
Badge style |
logo |
string | — | Icon: a custom icon slug, an octicon name, a simple-icons slug, or a data:image/svg+xml;base64,... URI |
logoColor |
hex | icon default | Recolors the icon (fill-based icons) |
logoWidth |
number | 14 (18 for for-the-badge) |
Icon width in px |
labelColor |
hex | 555555 (2b2b2b for for-the-badge) |
Label background |
label, message, color |
string | — | For static/v1 and dynamic badges |
Parameters (dynamic): url (required, http(s) JSON source ≤ 512 KB), query (required, jq-style path like $.a.b[0].c — keys, array indexes, bracketed keys), prefix, suffix, queryColor (use a queried value as the badge color).
The logo resolution chain mirrors custom-icon-badges:
data:URIs pass through as-is- Your icons:
icons/<slug>.svgin the repo configured byICON_REPO(default: this repo) - GitHub octicons (e.g.
logo=git-commit) - simple-icons brand icons (e.g.
logo=typescript)
Upload your own icon by adding icons/my-icon.svg to your fork/repo and pointing ICON_REPO at owner/repo.
Any other shields path — /api/badge/dynamic/yaml|xml|toml, /api/badge/github/stars/:user/:repo, /api/badge/npm/v/:package, … — is proxied to img.shields.io with the resolved custom icon injected, exactly the architecture custom-icon-badges uses. Static, static/v1, and dynamic/json badges are rendered natively; everything else stays shields-compatible through the proxy.
Badges are cached 6 hours per URL. Errors render as a small red badge instead of a broken image.
Endpoint: GET /api/videos
A reimplementation of the dynamic API from DenverCoder1/github-readme-youtube-cards — your latest videos as SVG cards, no GitHub Action required. Needs a YouTube Data API v3 key (see YOUTUBE_API_KEY).
<!-- by channel -->

<!-- by playlist -->
Parameters (same names as upstream):
| Parameter | Type | Default | Description |
|---|---|---|---|
channel_id |
string | — | YouTube channel id (or use playlist_id) |
playlist_id |
string | — | YouTube playlist id |
width |
number | 250 |
Card width in px |
border_radius |
number | 8 |
Card corner radius |
background_color |
hex | ffffff |
Card background |
title_color |
hex | 000000 |
Video title color |
stats_color |
hex | 000000 |
Views/date color |
max_title_lines |
number | 1 |
Lines to wrap long titles to (long text truncates with …) |
max_videos |
number | 6 |
Number of cards (1–50) |
filter |
regex | — | Exclude videos whose titles match, e.g. Shorts|Community |
Thumbnails are fetched and embedded as data URIs so the cards render inside GitHub's image proxy. Videos are cached 6 hours per URL.
Endpoint: GET /api/ascii
Converts any text into a pixel block SVG card using a built-in 5×7 bitmap font. No GitHub token required — works standalone.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
text |
string | required | Text to render. Max 50 characters. Auto-converted to uppercase. |
style |
block | outline | shadow | neon |
block |
Visual rendering style |
size |
sm | md | lg | xl |
md |
Block size preset (sets block_w, block_h, gap, char_spacing together) |
color |
hex | theme title color | Color of the pixel blocks (without #) |
block_w |
number | preset | Width of each pixel block in px — overrides size preset |
block_h |
number | preset | Height of each pixel block in px — overrides size preset |
gap |
number | preset | Gap between pixel blocks in px — overrides size preset |
char_spacing |
number | preset | Extra space between characters in px — overrides size preset |
block_radius |
number | 2 |
Border radius of each pixel block |
Size presets:
size |
block_w |
block_h |
gap |
char_spacing |
|---|---|---|---|---|
sm |
10 | 7 | 2 | 6 |
md (default) |
18 | 12 | 3 | 10 |
lg |
24 | 16 | 4 | 14 |
xl |
32 | 22 | 5 | 18 |
Style previews:
style |
Effect |
|---|---|
block |
Solid filled pixel blocks |
outline |
Hollow blocks — only the border is drawn |
shadow |
Solid blocks with a soft offset shadow behind them |
neon |
Solid blocks with an SVG glow filter — best on dark backgrounds |
Supported characters:
A–Z,0–9, and! ? . , : ; - _ + = * / # @ % & ( ) [ ] < > ^ ~ ' " ` | spaceLowercase input is auto-uppercased. Unsupported characters render as blank space.
Examples:
<!-- Default block style, dark theme -->

<!-- Outline style — hollow blocks -->

<!-- Shadow style — blocks with a soft drop shadow -->

<!-- Neon style — glowing blocks (best on dark backgrounds) -->

<!-- Large size preset -->

<!-- Small size, compact spacing -->

<!-- XL neon on dark — maximum impact -->

<!-- Outline + rounded blocks, transparent background -->
Apply a theme with &theme=NAME on any card.
| Name | Preview colors |
|---|---|
default |
Blue title, gray text, white background |
dark |
White title, gray text, black background |
radical |
Pink title, cyan text, dark background |
tokyonight |
Blue title, teal text, navy background |
dracula |
Pink title, cyan text, dark background |
gruvbox |
Yellow title, cream text, dark background |
onedark |
Gold title, red text, dark background |
transparent |
Purple title, transparent background |



Tip: Use
theme=transparentfor cards that adapt to GitHub's light/dark mode switching.
These parameters work on all cards (on the streak card, hide_title is inverted — the title is hidden by default, pass hide_title=false to show it):
| Parameter | Type | Default | Description |
|---|---|---|---|
theme |
string | default |
Named theme (see Themes) |
title_color |
hex | theme value | Title text color (without #), e.g. ff6e96 |
text_color |
hex | theme value | Body text color (without #) |
icon_color |
hex | theme value | Icon color (without #) |
bg_color |
hex | theme value | Background color (without #). Use 00000000 for transparent |
border_color |
hex | theme value | Border color (without #) |
hide_border |
boolean | false |
Remove the card border entirely |
hide_title |
boolean | false |
Hide the card title bar |
custom_title |
string | — | Replace the default title with custom text |
border_radius |
number | 4.5 |
Corner radius of the card in px |
Color override examples:
<!-- Custom colors (hex values, no # prefix) -->

<!-- No border, custom title -->

<!-- Fully transparent background -->
# 1. Clone and install
git clone <your-repo>
cd gh-stats
npm install
# 2. Build
npm run build
# 3. Run (set GITHUB_TOKEN first)
GITHUB_TOKEN=ghp_xxxxxxxxxxxx npm startFor development with live reload:
:: Windows CMD
set GITHUB_TOKEN=ghp_yourTokenHere && npm run dev# PowerShell
$env:GITHUB_TOKEN="ghp_yourTokenHere"; npm run dev# bash / Git Bash / macOS / Linux
GITHUB_TOKEN=ghp_yourTokenHere npm run devThe server starts on port 3000 by default. Set PORT to change it.
# Build
docker build -t gh-stats .
# Run
docker run -e GITHUB_TOKEN=ghp_xxxxxxxxxxxx -p 3000:3000 gh-statsFree tier includes 100,000 requests/day.
# 1. Install Wrangler (if not already installed)
npm install -g wrangler
wrangler login
# 2. Store your token as a secret (never commit it to wrangler.toml)
wrangler secret put GITHUB_TOKEN
# 2b. Optional secrets
wrangler secret put YOUTUBE_API_KEY # for /api/videos
# 3. Deploy
npm run deployYour card URL will be:
https://gh-stats.<your-subdomain>.workers.dev/api/stats?username=eru123
| Variable | Required | Description |
|---|---|---|
GITHUB_TOKEN |
For GitHub cards | GitHub Personal Access Token. Needs read:user and public_repo scopes. Generate at: GitHub → Settings → Developer settings → Personal access tokens |
YOUTUBE_API_KEY |
For YouTube cards | YouTube Data API v3 key — Google Cloud Console → Enable "YouTube Data API v3" → Credentials → API key |
ICON_REPO |
No | Repo holding custom badge icons, as owner/repo (icons live in icons/<slug>.svg). Default: this repo. Can also be a full raw-content base URL |
PORT |
No | Port for the Node.js server (default: 3000) |
CACHE_SECONDS |
No | Override the default cache TTL for all card types |
WHITELIST |
No | Comma-separated list of allowed usernames. If set, all other usernames are rejected. Useful for self-hosted instances. Example: eru123,octocat |