DEV Community

Cover image for Build Your Own GitHub Profile Widgets from Scratch
Boluwaji Akinsefunmi
Boluwaji Akinsefunmi

Posted on

Build Your Own GitHub Profile Widgets from Scratch

A complete guide to designing, building, and deploying custom SVG widgets for your GitHub profile README.


Why Build Your Own?

If you've spent any time customizing your GitHub profile, you've probably used widgets like github-readme-stats, github-profile-trophy, or github-readme-streak-stats. They work great, until they don't. The trophy widget went down. The streak stats hit rate limits. The stats card shows stale data.

The reality is that these are someone else's free Vercel deployments running on someone else's GitHub token. When they break, you're stuck.

Building your own widgets gives you full control over design, data, uptime, and updates. And it's far simpler than you might think. This tutorial walks you through the entire process, from understanding how these widgets actually work to deploying your own via GitHub Actions and eventually Vercel.


Table of Contents

  1. How GitHub Profile Widgets Work
  2. The Tech Stack
  3. Understanding SVGs for GitHub
  4. Fetching Data from the GitHub API
  5. Building Your First Widget Step by Step
  6. Widget Ideas and Patterns
  7. Styling and Design Principles
  8. Deployment Option (GitHub Actions)
  9. Advanced Techniques
  10. Troubleshooting
  11. Project Template

1. How GitHub Profile Widgets Work

Every widget you see on a GitHub profile README follows the same pattern:

Your README.md contains an <img> tag
    ↓
The src points to a URL (e.g., some-api.vercel.app/api/widget?username=you)
    ↓
That URL is a server that:
    1. Receives your username as a query parameter
    2. Calls the GitHub API to fetch your stats
    3. Generates an SVG image dynamically
    4. Returns it with Content-Type: image/svg+xml
    ↓
GitHub's Markdown renderer displays the SVG inline
Enter fullscreen mode Exit fullscreen mode

That's it. There's no magic. The "widget" is just a dynamically generated image.

There are two deployment models:

Live API (Vercel/Cloudflare Workers): The SVG is generated on every request. Always fresh, but depends on external hosting and can hit rate limits.

GitHub Actions (pre-generated): A scheduled workflow runs your script, generates SVG files, commits them to your repo. The README references local files (./widgets/my-widget.svg). No external dependencies, always available, but only updates on schedule.

This tutorial covers both approaches.


2. The Tech Stack

You need surprisingly little:

Language: Python (for GitHub Actions) or Node.js/TypeScript (for Vercel). This tutorial uses Python, but the concepts translate directly.

Data Source: The GitHub REST API or GraphQL API. The REST API is simpler; GraphQL gives you contribution data that REST can't access.

Output Format: SVG (Scalable Vector Graphics). This is the only format that makes sense, it's text-based (easy to generate programmatically), scales perfectly at any size, supports styling, and GitHub renders it natively.

Deployment: GitHub Actions (Phase 1) or Vercel Serverless Functions (Phase 2).

Dependencies: Just requests for Python (to call the GitHub API). That's it. No SVG libraries needed.


3. Understanding SVGs for GitHub

SVG is an XML-based image format. If you can write HTML, you can write SVG. Here's a minimal example:

<svg xmlns="http://www.w3.org/2000/svg" width="400" height="200" viewBox="0 0 400 200">
  <rect width="400" height="200" rx="12" fill="#ffffff" stroke="#e5e5e5"/>
  <text x="24" y="40" font-size="18" font-weight="600" fill="#1d1d1f">
    Hello, GitHub!
  </text>
  <text x="24" y="64" font-size="13" fill="#86868b">
    This is a custom widget.
  </text>
  <rect x="24" y="80" width="200" height="8" rx="4" fill="#f0f0f0"/>
  <rect x="24" y="80" width="140" height="8" rx="4" fill="#007aff"/>
</svg>
Enter fullscreen mode Exit fullscreen mode

That gives you a card with a title, subtitle, and a progress bar. No libraries needed.

What GitHub Supports in SVGs

GitHub proxies all images through camo.githubusercontent.com, which means some SVG features are stripped or blocked for security. Here's what works and what doesn't:

Works:

  • Basic shapes: <rect>, <circle>, <line>, <path>, <polygon>
  • Text: <text>, <tspan>
  • Gradients: <linearGradient>, <radialGradient>
  • Filters: <filter>, <feDropShadow>, <feGaussianBlur> (with limits)
  • Clipping: <clipPath>
  • Groups: <g> with transforms
  • Inline styles and <style> blocks
  • Emoji in text elements (renders as text, not images)
  • @import url() for Google Fonts (works but may be slow to load)
  • Defs and reusable elements

Does NOT work:

  • <script> tags (stripped for security)
  • <foreignObject> (stripped)
  • External images via <image href="..."> (blocked by CSP)
  • CSS animations with @keyframes (stripped by camo proxy)
  • <animate> / SMIL animations (stripped by camo proxy)
  • Embedded base64 images (blocked)
  • Fetching external resources

The main limitation is no interactivity and no animations when viewed on GitHub. Your SVGs need to be static but visually compelling.

SVG Coordinate System

SVGs use a coordinate system where (0,0) is the top-left corner:

(0,0) ──────────────── (width, 0)
  │                        │
  │     Your canvas        │
  │                        │
(0, height) ─────── (width, height)
Enter fullscreen mode Exit fullscreen mode

The viewBox attribute defines the internal coordinate system. If your viewBox is 0 0 800 300, you work with coordinates from 0-800 horizontally and 0-300 vertically, regardless of the actual rendered size.

Key SVG Elements You'll Use

<!-- Rectangle with rounded corners -->
<rect x="10" y="10" width="200" height="100" rx="8" fill="#fff" stroke="#eee" stroke-width="1"/>

<!-- Circle -->
<circle cx="50" cy="50" r="20" fill="#007aff"/>

<!-- Text -->
<text x="10" y="30" font-family="Arial" font-size="14" fill="#333">Hello</text>

<!-- Line -->
<line x1="0" y1="100" x2="400" y2="100" stroke="#eee" stroke-width="1"/>

<!-- Path (for complex shapes) -->
<path d="M 10 80 Q 50 10 90 80 T 170 80" stroke="#007aff" fill="none" stroke-width="2"/>

<!-- Linear gradient -->
<defs>
  <linearGradient id="grad1" x1="0" y1="0" x2="1" y2="0">
    <stop offset="0%" stop-color="#007aff"/>
    <stop offset="100%" stop-color="#5856d6"/>
  </linearGradient>
</defs>
<rect fill="url(#grad1)" ... />
Enter fullscreen mode Exit fullscreen mode

4. Fetching Data from the GitHub API

REST API Basics

The GitHub REST API is the simplest way to get profile data. Here's how to fetch what you need:

import requests

GITHUB_TOKEN = "ghp_your_token_here"  # Optional but recommended
USERNAME = "your-username"

headers = {
    "Accept": "application/vnd.github.v3+json",
    "Authorization": f"token {GITHUB_TOKEN}"  # Remove this line if no token
}

# Get user profile
user = requests.get(
    f"https://api.github.com/users/{USERNAME}",
    headers=headers
).json()

# Get repositories (up to 100)
repos = requests.get(
    f"https://api.github.com/users/{USERNAME}/repos",
    headers=headers,
    params={"per_page": 100, "sort": "updated"}
).json()

# Get languages for a specific repo
languages = requests.get(
    f"https://api.github.com/repos/{USERNAME}/some-repo/languages",
    headers=headers
).json()
# Returns: {"Python": 45000, "JavaScript": 12000, ...} (bytes of code)

# Get recent public events (activity)
events = requests.get(
    f"https://api.github.com/users/{USERNAME}/events/public",
    headers=headers,
    params={"per_page": 100}
).json()
Enter fullscreen mode Exit fullscreen mode

What Data Is Available

Endpoint What You Get Use For
/users/{user} Profile info, follower/following counts, public repo count Header stats
/users/{user}/repos All repos with name, language, stars, size, dates Skyline, skill tree
/repos/{user}/{repo}/languages Language breakdown in bytes Language charts
/users/{user}/events/public Last 90 days of public activity Activity widgets
/search/commits Commit search results Commit patterns

Rate Limits

Without a token: 60 requests/hour. With a token: 5,000 requests/hour. For GitHub Actions, the built-in GITHUB_TOKEN gives you 1,000 requests/hour, which is plenty.

If you're iterating over repos to get language data, that's one API call per repo. For 50 repos, that's 50 calls — well within limits.

GraphQL API (For Contribution Data)

The REST API doesn't expose your contribution graph data. For that, you need GraphQL:

query = """
query($username: String!) {
  user(login: $username) {
    contributionsCollection {
      totalCommitContributions
      totalPullRequestContributions
      totalIssueContributions
      contributionCalendar {
        totalContributions
        weeks {
          contributionDays {
            contributionCount
            date
            weekday
          }
        }
      }
    }
  }
}
"""

response = requests.post(
    "https://api.github.com/graphql",
    headers={"Authorization": f"Bearer {GITHUB_TOKEN}"},
    json={"query": query, "variables": {"username": USERNAME}}
)
data = response.json()["data"]["user"]["contributionsCollection"]
Enter fullscreen mode Exit fullscreen mode

This gives you the actual green-squares contribution data this is something that most third-party widgets can't even access without a user token.


5. Building Your First Widget Step by Step

Let's build a "Language Bar" widget from scratch. It shows your top languages as a horizontal stacked bar with labels — similar to the language bar on a GitHub repo, but for your entire profile.

Step 1: Gather the Data

from collections import defaultdict

# Language color mapping
LANG_COLORS = {
    "JavaScript": "#F7DF1E", "TypeScript": "#3178C6",
    "Python": "#3776AB", "HTML": "#E34F26",
    "CSS": "#1572B6", "Java": "#ED8B00",
    "Go": "#00ADD8", "Rust": "#DEA584",
    "Ruby": "#CC342D", "PHP": "#777BB4",
    "Swift": "#F05138", "Kotlin": "#7F52FF",
    "Shell": "#89E051", "C++": "#00599C",
    "Vue": "#4FC08D", "Dart": "#0175C2",
}

def get_language_stats(username, headers):
    """Aggregate language bytes across all repos."""
    repos = requests.get(
        f"https://api.github.com/users/{username}/repos",
        headers=headers,
        params={"per_page": 100, "type": "owner"}
    ).json()

    totals = defaultdict(int)
    for repo in repos:
        if repo.get("fork"):
            continue  # Skip forks
        langs = requests.get(
            f"https://api.github.com/repos/{username}/{repo['name']}/languages",
            headers=headers
        ).json()
        for lang, bytes_count in langs.items():
            totals[lang] += bytes_count

    return dict(sorted(totals.items(), key=lambda x: x[1], reverse=True))
Enter fullscreen mode Exit fullscreen mode

Step 2: Build the SVG Generator

Think of your SVG generator as a function that takes data and returns a string:

def generate_language_bar(languages, username):
    width = 600
    height = 160
    total = sum(languages.values())

    # Start the SVG
    svg = f'''<svg xmlns="http://www.w3.org/2000/svg"
         width="{width}" height="{height}"
         viewBox="0 0 {width} {height}">

  <!-- Card background -->
  <rect width="{width}" height="{height}" rx="12"
        fill="#ffffff" stroke="#e8e8ed" stroke-width="1"/>

  <!-- Title -->
  <text x="24" y="36" font-family="Arial, sans-serif"
        font-size="16" font-weight="600" fill="#1d1d1f">
    Languages
  </text>
  <text x="24" y="54" font-family="Arial, sans-serif"
        font-size="12" fill="#86868b">
    @{username} · across all repositories
  </text>
'''

    # Draw the stacked bar
    bar_x = 24
    bar_y = 72
    bar_width = width - 48
    bar_height = 12
    current_x = bar_x

    # Clip path for rounded corners
    svg += f'''  <clipPath id="barClip">
    <rect x="{bar_x}" y="{bar_y}" width="{bar_width}"
          height="{bar_height}" rx="6"/>
  </clipPath>
  <g clip-path="url(#barClip)">\n'''

    top_langs = list(languages.items())[:8]

    for lang, bytes_count in top_langs:
        segment_width = (bytes_count / total) * bar_width
        if segment_width < 1:
            continue
        color = LANG_COLORS.get(lang, "#86868b")
        svg += f'    <rect x="{current_x:.1f}" y="{bar_y}" '
        svg += f'width="{segment_width:.1f}" height="{bar_height}" '
        svg += f'fill="{color}"/>\n'
        current_x += segment_width

    svg += '  </g>\n'

    # Legend dots and labels
    legend_y = 108
    col = 0
    row = 0
    for lang, bytes_count in top_langs:
        pct = bytes_count / total * 100
        color = LANG_COLORS.get(lang, "#86868b")
        x = 24 + col * 180
        y = legend_y + row * 22

        svg += f'  <circle cx="{x + 5}" cy="{y}" r="4" fill="{color}"/>\n'
        svg += f'  <text x="{x + 14}" y="{y + 4}" font-family="Arial" '
        svg += f'font-size="11" fill="#424245">{lang} {pct:.1f}%</text>\n'

        col += 1
        if col >= 3:
            col = 0
            row += 1

    svg += '</svg>\n'
    return svg
Enter fullscreen mode Exit fullscreen mode

Step 3: Save It

svg_content = generate_language_bar(languages, "your-username")

import os
os.makedirs("widgets", exist_ok=True)
with open("widgets/language-bar.svg", "w") as f:
    f.write(svg_content)
Enter fullscreen mode Exit fullscreen mode

Step 4: Reference in Your README

<div align="center">
  <img src="./widgets/language-bar.svg" alt="Language Bar" width="100%" />
</div>
Enter fullscreen mode Exit fullscreen mode

That's the entire process. Every widget you build follows these four steps: gather data → generate SVG string → save to file → reference in README.


6. Widget Ideas and Patterns

Here are widget patterns categorized by complexity.

Beginner

Profile Stats Card: Display followers, repos, stars, and commits in a clean card layout. Straightforward rectangles and text positioning.

Language Pie/Bar Chart: Show your language distribution. Use SVG <circle> with stroke-dasharray for a donut chart, or stacked <rect> for a bar.

Social Links Banner: A styled banner with your social media handles. Simple text and color work.

Intermediate

Contribution Heatmap: Recreate the GitHub contribution graph with custom colors and styling. Requires GraphQL API for contribution data. It's a grid of small <rect> elements with varying fill opacity.

Activity Timeline: A vertical timeline showing your recent commits, PRs, and issues. Use the Events API and draw lines with circle markers.

Repo Showcase Card: A card highlighting your top-starred repos with descriptions, languages, and star counts.

Advanced

Code DNA: Generate a unique visual fingerprint from your coding patterns. Use trigonometric functions to create a double helix where colors represent languages.

Repo Skyline: A city skyline visualization where each building is a repo. Height maps to codebase size, color to primary language.

Skill Tree: An RPG-style tech tree showing your frameworks and languages with XP bars calculated from actual usage data.

Code Weather: A weather forecast metaphor for your coding activity. Commit frequency maps to weather conditions.


7. Styling and Design Principles

Typography in SVGs

GitHub's camo proxy doesn't always load external fonts reliably. Use system font stacks:

<text font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI',
      Helvetica, Arial, sans-serif" font-size="14" fill="#1d1d1f">
  Your text here
</text>
Enter fullscreen mode Exit fullscreen mode

If you want to use Google Fonts, import them in a <style> block inside <defs>:

<defs>
  <style>
    @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&amp;display=swap');
  </style>
</defs>
Enter fullscreen mode Exit fullscreen mode

This adds a network request. On GitHub, font loading can be unreliable, so always provide fallbacks.

Design Tips for Great-Looking Widgets

Consistency matters. Pick a color palette and stick to it across all your widgets. If you go with Apple's design language, use their colors everywhere: #1D1D1F for primary text, #86868B for secondary, #007AFF for accents.

Generous padding. Don't cram content to the edges. Leave at least 24-32px of padding inside your card. White space makes everything look more polished.

Rounded corners. Use rx="12" or rx="16" on your card backgrounds. Sharp corners look dated.

Subtle shadows. A single feDropShadow with low opacity adds depth without being heavy:

<filter id="shadow">
  <feDropShadow dx="0" dy="1" stdDeviation="3"
                flood-color="#000" flood-opacity="0.04"/>
</filter>
<rect filter="url(#shadow)" ... />
Enter fullscreen mode Exit fullscreen mode

Text hierarchy. Use font-weight and size to create visual levels, not color variety. A good system: title at 18px/600 weight, subtitle at 13px/400, labels at 11px/500 uppercase, values at 14px/600.

Limit your palette. Two or three accent colors maximum per widget. Language colors already add variety, your UI chrome should stay neutral.

Color Palettes Worth Trying

Apple Minimal (Light):
Background #FFFFFF, Card #F5F5F7, Text #1D1D1F, Secondary #86868B, Accent #007AFF

GitHub Dark:
Background #0D1117, Card #161B22, Text #E6EDF3, Secondary #8B949E, Accent #58A6FF

Warm Neutral:
Background #FAF9F6, Card #FFFFFF, Text #2D2D2D, Secondary #999, Accent #E07A5F


8. Deployment Option (Using GitHub Actions)

This is the simplest approach. A GitHub Action runs your Python script on a schedule, generates SVG files, and commits them to your repo.

Project Structure

your-username/
├── .github/
│   └── workflows/
│       └── update-widgets.yml
├── scripts/
│   └── generate_widgets.py
├── widgets/                    (auto-generated — don't edit by hand)
│   ├── widget-1.svg
│   └── widget-2.svg
├── README.md
└── requirements.txt
Enter fullscreen mode Exit fullscreen mode

The Workflow File

Create .github/workflows/update-widgets.yml:

name: Update Profile Widgets

on:
  schedule:
    - cron: '0 6 * * *'         # Daily at 6 AM UTC
  workflow_dispatch:              # Manual trigger from Actions tab
  push:
    branches: [main]
    paths:
      - 'scripts/generate_widgets.py'

permissions:
  contents: write                 # Needed to push commits

jobs:
  update-widgets:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pip install requests

      - name: Generate widgets
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_USERNAME: ${{ github.repository_owner }}
          OUTPUT_DIR: widgets
        run: python scripts/generate_widgets.py

      - name: Commit and push
        run: |
          git config --local user.email "action@github.com"
          git config --local user.name "GitHub Action"
          git add widgets/ README.md
          git diff --quiet && git diff --staged --quiet || (
            git commit -m "🎨 Update profile widgets [skip ci]"
            git push
          )
Enter fullscreen mode Exit fullscreen mode

Key details:

The GITHUB_TOKEN is automatically provided by GitHub Actions — you don't need to create any secrets. It has read access to your public repos, which is all you need.

The [skip ci] in the commit message prevents the workflow from triggering itself in an infinite loop.

The git diff --quiet check ensures we only commit if files actually changed. This keeps your commit history clean.

workflow_dispatch lets you trigger the workflow manually from the Actions tab — useful during development.

Testing Locally

Before pushing, test your script locally:

# Set your token (optional for public repos, but increases rate limit)
export GITHUB_TOKEN="ghp_your_personal_access_token"
export GITHUB_USERNAME="your-username"
export OUTPUT_DIR="widgets"

# Run it
python scripts/generate_widgets.py

# Check the output
ls -la widgets/

# Open an SVG in your browser to preview
open widgets/my-widget.svg       # macOS
xdg-open widgets/my-widget.svg   # Linux
Enter fullscreen mode Exit fullscreen mode

Debugging in Actions

If the workflow fails, check the Actions tab in your repo. Common issues:

  • Permission denied on push: Make sure permissions: contents: write is set in the workflow.
  • API rate limit: With GITHUB_TOKEN, you get 1,000 req/hr. If you have many repos, optimize by reducing API calls.
  • Empty diff: If widgets didn't change, the commit step is skipped silently. This is expected and correct.

9. Advanced Techniques

Generating Smooth Curves

For widgets like Code DNA that need organic shapes, use trigonometric functions:

import math

points = []
for i in range(100):
    t = i / 100
    x = start_x + t * total_width
    y = center_y + math.sin(t * math.pi * 4) * amplitude
    points.append((x, y))

# Convert to SVG path
path = f"M {points[0][0]:.1f} {points[0][1]:.1f}"
for x, y in points[1:]:
    path += f" L {x:.1f} {y:.1f}"

svg += f'<path d="{path}" stroke="#007aff" fill="none" stroke-width="2"/>'
Enter fullscreen mode Exit fullscreen mode

For smoother curves, use quadratic or cubic Bézier path commands (Q or C) instead of line segments (L).

Creating Unique Visual Fingerprints

To make each user's widget visually distinct, hash their data and use the hash to seed visual parameters:

import hashlib
import json

data_string = json.dumps(sorted(user_languages.items()))
hash_hex = hashlib.md5(data_string.encode()).hexdigest()

# Extract deterministic parameters from hash
phase = int(hash_hex[:4], 16) / 65535 * math.pi * 2
frequency = 0.8 + (int(hash_hex[4:8], 16) / 65535) * 0.6
amplitude = 30 + (int(hash_hex[8:12], 16) / 65535) * 20
Enter fullscreen mode Exit fullscreen mode

Every user gets a deterministically unique pattern that's consistent across regenerations but visually different from everyone else's.

SVG Patterns and Textures

Add visual interest with SVG patterns:

<defs>
  <pattern id="dots" width="10" height="10" patternUnits="userSpaceOnUse">
    <circle cx="5" cy="5" r="1" fill="#000" opacity="0.05"/>
  </pattern>
</defs>
<rect fill="url(#dots)" width="800" height="300"/>
Enter fullscreen mode Exit fullscreen mode

Responsive Width

Make your widgets work at any display size using viewBox and percentage widths:

<svg viewBox="0 0 800 300" width="100%">
  <!-- Content designed for 800x300, scales to any width -->
</svg>
Enter fullscreen mode Exit fullscreen mode

In your README:

<img src="./widgets/my-widget.svg" width="100%" />
Enter fullscreen mode Exit fullscreen mode

Dark Mode Support

GitHub supports dark mode. You can create two versions of each widget and use GitHub's built-in theme-switching syntax:

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="./widgets/my-widget-dark.svg" />
  <source media="(prefers-color-scheme: light)" srcset="./widgets/my-widget-light.svg" />
  <img alt="Widget" src="./widgets/my-widget-light.svg" />
</picture>
Enter fullscreen mode Exit fullscreen mode

This is a native GitHub Markdown feature that swaps images based on the viewer's system theme. For Vercel deployments, use a ?theme=dark query parameter instead.


10. Troubleshooting

Widget Not Updating on GitHub

GitHub aggressively caches images through its camo proxy. If you've updated your SVG but the old version still shows, try changing the image URL slightly (append ?v=2), or simply wait — the cache typically expires within a few hours.

SVG Looks Different on GitHub vs. Local Browser

GitHub's camo proxy sanitizes SVGs. If your widget renders differently on GitHub, check for unsupported features (animations, scripts, foreignObject), make sure fonts have proper fallbacks, and test by viewing the raw SVG file on GitHub rather than the rendered README.

GitHub API Returns 403

You've hit the rate limit. Add authentication (the Actions GITHUB_TOKEN increases limits to 1,000/hr), cache API responses during local development, and reduce the number of API calls by batching requests.

SVG Shows as a Broken Image

Common causes: invalid XML (unclosed tags, unescaped & — use &amp; instead), a missing xmlns="http://www.w3.org/2000/svg" attribute on the root element, or the file is empty. Validate by opening the SVG directly in a browser.

Text Alignment Is Off

SVG text anchoring works differently than HTML:

<!-- Left-aligned (default): x is the LEFT edge -->
<text x="100" y="50">Left</text>

<!-- Center-aligned: x is the CENTER point -->
<text x="100" y="50" text-anchor="middle">Center</text>

<!-- Right-aligned: x is the RIGHT edge -->
<text x="100" y="50" text-anchor="end">Right</text>
Enter fullscreen mode Exit fullscreen mode

The y coordinate is the text baseline (bottom of letters), not the top. So y="50" means the bottom of the text sits at y=50.


11. Project Template

Here's a minimal, copy-paste-ready template to get you started immediately:

#!/usr/bin/env python3
"""
Minimal GitHub Profile Widget Generator
Modify generate_widget() and you're done.
"""

import os
import requests
from collections import defaultdict

USERNAME = os.environ.get("GITHUB_USERNAME", "your-username")
TOKEN = os.environ.get("GITHUB_TOKEN", "")
OUTPUT = os.environ.get("OUTPUT_DIR", "widgets")

def github_get(endpoint, params=None):
    headers = {"Accept": "application/vnd.github.v3+json"}
    if TOKEN:
        headers["Authorization"] = f"token {TOKEN}"
    resp = requests.get(f"https://api.github.com{endpoint}",
                        headers=headers, params=params, timeout=30)
    return resp.json() if resp.status_code == 200 else {}

def fetch_data():
    user = github_get(f"/users/{USERNAME}")
    repos = github_get(f"/users/{USERNAME}/repos",
                       {"per_page": 100, "sort": "updated"})
    languages = defaultdict(int)
    for repo in (repos if isinstance(repos, list) else []):
        if not repo.get("fork"):
            langs = github_get(
                f"/repos/{USERNAME}/{repo['name']}/languages")
            for lang, count in langs.items():
                languages[lang] += count
    return {"user": user, "repos": repos, "languages": dict(languages)}

def generate_widget(data):
    width, height = 600, 120
    name = data["user"].get("name", USERNAME)
    repos = data["user"].get("public_repos", 0)
    followers = data["user"].get("followers", 0)

    return f'''<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}"
     viewBox="0 0 {width} {height}">
  <rect width="{width}" height="{height}" rx="12"
        fill="#fff" stroke="#e8e8ed" stroke-width="1"/>
  <text x="24" y="40" font-family="Arial, sans-serif"
        font-size="18" font-weight="600" fill="#1d1d1f">{name}</text>
  <text x="24" y="64" font-family="Arial, sans-serif"
        font-size="13" fill="#86868b">
    @{USERNAME} · {repos} repos · {followers} followers
  </text>
  <text x="24" y="96" font-family="Arial, sans-serif"
        font-size="12" fill="#aeaeb2">
    {len(data["languages"])} languages across all repositories
  </text>
</svg>
'''

if __name__ == "__main__":
    os.makedirs(OUTPUT, exist_ok=True)
    data = fetch_data()
    svg = generate_widget(data)
    path = os.path.join(OUTPUT, "my-widget.svg")
    with open(path, "w") as f:
        f.write(svg)
    print(f"Generated {path}")
Enter fullscreen mode Exit fullscreen mode

Pair this with the workflow YAML from Section 8, push it to your profile repo, and you're live.


Top comments (0)