DEV Community

Cover image for Your GitHub profile is wallpaper. Mine is a spaceship.
Rudratosh Shastri
Rudratosh Shastri

Posted on

Your GitHub profile is wallpaper. Mine is a spaceship.

Real-time data wired to SVG art

Here's an uncomfortable truth: your GitHub profile README is the most-viewed file you'll ever write, and almost nobody reads it. It's the first thing that loads on your profile — and it's usually a pile of badges, a streak counter, and a "most used languages" card that looks exactly like everyone else's. Wallpaper. You scroll past your own.

I snapped one evening and decided mine would do something. Not look busy — do something. Two weeks later my profile is a mission console orbiting a living planet, and every panel on it is a real instrument wired to my actual data. It rebuilds itself every morning while I'm asleep.

This is the whole teardown — the one weird constraint that makes this genuinely hard, the SVG trick that beats it, the maths behind the rings, and the three bugs that stole my weekend. Steal all of it.

Deep Field Survey — the hero console: a planet wrapped in contribution rings, with a live readout
The hero. The rings are my contribution calendar; the readout is live.

The rule I made before writing any code

I wrote one sentence on a sticky note:

If it moves, it has to mean something.

That rule is the entire difference between a toy and a dashboard, and it was brutal. It killed half my ideas on the spot. No glowing lines for vibes. No waveform that's just a pretty sine. Every animated pixel has to be a real number — my commits, my pull requests, my languages, my writing — or it doesn't ship.

Hold that thought, because I break it later and have to fix it.

First, the problem nobody warns you about

I sat down to animate my profile and immediately hit a wall:

GitHub READMEs can't run JavaScript. None. No <script>, no <canvas>, no WebGL, no click handlers. GitHub strips all of it for security. You get Markdown and images. That's the whole sandbox.

So how is anything on my profile moving?

Here's the trick, and it's the most useful thing in this post: an SVG animates itself even when it's just an <img>. GitHub strips scripts, but it keeps declarative animation — plain CSS @keyframes and SMIL baked into the SVG. Drop this in a README:

<svg xmlns="http://www.w3.org/2000/svg" width="200" height="60">
  <style>
    .dot { animation: pulse 1.8s ease-in-out infinite; }
    @keyframes pulse { 50% { opacity: .2; } }
  </style>
  <circle class="dot" cx="30" cy="30" r="8" fill="#5bd6ff"/>
</svg>
Enter fullscreen mode Exit fullscreen mode

…reference it as ![](badge.svg), and that dot breathes. Forever. Inside a file GitHub swore couldn't run code.

That one loophole is the foundation of everything below. Every spinning ring, radar sweep, and flickering readout is just an SVG pretending to be a picture.

Now let's build the console.

The planet wears my contribution graph as rings

The hero is a slowly rotating planet, and the rings around it aren't decoration — they're my contribution calendar bent into orbit.

Seven rings, one per weekday. Each ring is split into 53 segments, one per week. A segment lights up — cyan, then mint, then gold — based on how hard I committed that day. A lazy month shows as thin, dim inner arcs. A sprint lights up a bright band around the equator.

The trick that makes it cheap is a one-line SVG superpower: pathLength. I tell each ring "your circumference is 53 units long," so now one unit = one week, and a dash pattern can paint activity week by week:

// each ring is one weekday; 53 segments = 53 weeks of the year
<ellipse pathLength="53"
         stroke-dasharray="0 12  0.7 0.3  0 40"  /* gap, dash(week), gap... */
         ... />
Enter fullscreen mode Exit fullscreen mode

A slow drift animation spins the whole system so it feels alive:

.ring { animation: ringdrift 90s linear infinite; }
@keyframes ringdrift { to { stroke-dashoffset: -53; } }
Enter fullscreen mode Exit fullscreen mode

One last touch sells the 3D: I clip the rings into a front half and a back half and draw them on either side of the planet, so they pass behind it at the top and in front at the bottom. Without that, it's a sticker. With it, the planet sits inside its own orbit.

A radar, a spectrum, and the signal I got wrong

Three more instruments, three more promises kept to that sticky note.

The radar is my open-source footprint — the pull requests I've opened in other people's repos, the work that never shows up in your own repo list. Each blip is a real PR. The dish sweeps, the blips ping as it passes.

Radar panel — external pull requests as blips on a sweeping scope

The spectrum turns my languages into emission lines, like you're reading the light off a star. Fat cyan line? TypeScript. Thin green sliver? Shell. The widths are the real percentages, not a vibe.

Spectrum panel — languages rendered as emission lines

The signal is the one I'm proudest of — because I first built it wrong. For about ten minutes it was a gorgeous sine wave… that meant absolutely nothing. It broke the rule, so I ripped it out and plotted what it should have been all along: my reactions per post on dev.to.

Now the tallest spike on the oscilloscope is my most-reacted article. The sweep "intercepts" each post in turn. It reads like a transmission log and it's 100% my real engagement. The second I swapped the fake wave for real numbers, the panel went from screensaver to dashboard. That's the whole thesis in one commit.

Signal panel — reactions per post plotted as an oscilloscope
The signal: every point is a post, the spike is my most-reacted one.

Ten files pretending to be one screen

The profile looks like a single console, but it's actually ten separate SVGs stacked in the README — hero, link badges, missions, radar, spectrum, signal, footer:

![hero](./svg/hero.svg)
![missions](./svg/missions.svg)
![radar](./svg/ops.svg)
![spectrum](./svg/spectrum.svg)
![signal](./svg/transmissions.svg)
Enter fullscreen mode Exit fullscreen mode

Why split them? One giant SVG is miserable to maintain, and every panel is a different height. The catch: stacked images show hairline seams and the illusion cracks. The fix is shared chrome — every panel draws the same corner brackets, the same bezel, and the same scanline sweeping down it, so ten images read as one continuous screen:

.scan { animation: scan 8s linear infinite; }
@keyframes scan { from { transform: translateY(-90px); } to { transform: translateY(100%); } }
Enter fullscreen mode Exit fullscreen mode

Consistency is the trick that turns a stack of pictures into one machine.

Missions panel — flagship repos as mission dossiers
One of the ten panels — flagship repos as mission dossiers, each with its own orbiting moon.

It files its own report every morning

Here's the part that still feels like magic to me: I never touch it.

A scheduled GitHub Action runs a small Node script every day, pulls fresh data, regenerates all ten SVGs, and commits them if anything changed:

on:
  schedule: [{ cron: '17 3 * * *' }]   # 03:17, not 03:00 — see below
  workflow_dispatch:
Enter fullscreen mode Exit fullscreen mode

(Why :17 and not :00? Because every cron job on Earth fires on the hour and GitHub's schedulers get jammed. Seventeen past is quiet, so it actually runs on time. Learned that the annoying way.)

The data comes from two places. GitHub's GraphQL API hands me the entire contribution calendar in one call, and I map its intensity levels straight onto the ring colors:

const LEVEL = { NONE:0, FIRST_QUARTILE:1, SECOND_QUARTILE:2, THIRD_QUARTILE:3, FOURTH_QUARTILE:4 };
weeks[w][day] = LEVEL[contributionLevel];   // → which ring segment lights, and how bright
Enter fullscreen mode Exit fullscreen mode

The writing signal comes from dev.to's public API — no key required, so the Action needs no extra secret:

const posts  = await (await fetch(`https://dev.to/api/articles?username=${me}&per_page=30`)).json();
const signal = posts.slice(0, 16).map(p => p.public_reactions_count).reverse();  // the oscilloscope
Enter fullscreen mode Exit fullscreen mode

Three habits that keep it boringly reliable: it fails soft (an API hiccup keeps yesterday's numbers instead of shipping a blank console), it commits only on a real change (no noisy history), and the token is a fine-grained, read-only PAT living in Actions secrets, never the repo.

Three bugs that ate my weekend

Every project has the bugs that teach you something. Here are mine, worst to dumbest.

1. The 7 MB planet that wouldn't spin. I spent a giddy hour baking a 72-frame rotating globe into the hero SVG… then learned that when GitHub renders an SVG as an image, a nested animation freezes on frame one — and the file had bloated to 7 MB. Lesson: declarative SVG animation survives the image sandbox; a video stuffed inside it does not. So the globe is a crisp still, and the rings do the spinning. (The actually-rotating planet lives in the standalone web version, where real motion is allowed.)

2. The bars that refused to exist. A row of stat bars just… didn't render. No error. Empty tracks. The cause? Each bar was a <span>, and inline elements ignore width and height. One display:block and every bar snapped into place. Ten years in, and CSS still finds new ways to make me feel like it's day one.

3. The fonts that lied. Everything looked perfect on my machine and slightly wrong on everyone else's — a title wrapping one character early, kerning off. Silent system-font fallback. The fix: subset the font to only the ~80 glyphs I actually use and embed it as base64, so the type is pixel-identical everywhere and still tiny.

So… was it worth it?

Honest damage report: ten hand-built SVG generators, a ring-projection system, a font-subsetting step, a two-source data pipeline, and a daily robot — for a file people glance at for four seconds.

Worth it. Here's why. A profile that just looks cool is a one-time hit — screenshot, move on. But a profile where the rings are my calendar, the radar is my open-source trail, and the signal is my readership is something sneakier: a status board for a career. It quietly rewards me for shipping and it's honest when I don't. That accountability turned out to be the real feature — and it made me care about my profile again, which is the small true thing under all the sci-fi.

Build your own

The whole generator is open and made to drop into any profile repo — change your username in one JSON file, copy the scripts and the Action, add the markers to your README, run it once from the Actions tab. After that it flies itself.

It's live on my profile right now. Go watch it turn:

→ github.com/rudratoshs

If you wire up your own instrument panel, tag me — I really want to see which numbers you choose to put on the dials. Follow along here for more "turn your dev data into something alive" teardowns. 👋

One question for you: if you could put a single number from your dev life on a dial the whole world could see — commits, PRs, reactions, review time, cups of coffee — which one would you pick? And be honest: would making it visible actually make you ship more? 👇

Top comments (4)

Collapse
 
rudratosh profile image
Rudratosh Shastri •

Câu hỏi hay — và bạn đã chạm đúng những chỗ khó. Trả lời từng phần:

  • "Rebuilds itself every morning": GitHub Actions scheduled workflow (cron), chạy mỗi sáng + cho phép workflow_dispatch để trigger tay. Mẹo nhỏ: đừng để 0 * * * * — mọi cron trên đời fire đúng giờ tròn nên scheduler của GitHub bị nghẽn; mình để :17 phút cho chắc.
  • Dữ liệu: contribution calendar lấy qua GraphQL API trong một call (REST sẽ tốn nhiều request hơn nhiều); reactions/bài viết lấy từ dev.to public API.
  • Visualisation: inline SVG, không canvas-to-data-URI. Vì GitHub render SVG như <img>, animation phải là declarative — CSS @keyframes / SMIL bên trong SVG. Animation lồng kiểu video/webp sẽ đứng ở frame 1.
  • Phần bạn hỏi khó nhất — cache/file size + invalidation: đúng là GitHub cache ảnh rất "lì". Hai điều mình làm: (1) workflow commit lại chính các file SVG mỗi ngày, nên URL raw đổi nội dung → camo cache bị bust tự nhiên; (2) giữ file nhỏ — font subset (chỉ ~80 glyph thật dùng) nhúng base64, hành tinh là ảnh tĩnh còn các ring mới quay, nên không bị phình 7MB.

Nếu bạn từng gặp giới hạn kích thước khi nhúng SVG động — đó gần như luôn là do nhúng raster/webp bên trong. Giữ nó thuần vector + declarative là thoát. Bạn đang định dựng kiểu dashboard nào cho profile của mình?

Collapse
 
svgicons profile image
Svg/icons •

Since GitHub embeds these SVGs as images, have you tested screen readers with richer Markdown alt text? The internal SVG semantics may not be reliably exposed.

Collapse
 
rudratosh profile image
Rudratosh Shastri •

Great catch — and you've put your finger on the real constraint.

You're exactly right: once GitHub renders the SVG as an <img>, the internal semantics are gone. Any <title>, <desc>, role="img" or aria-label inside the SVG is sealed behind the image boundary — a screen reader sees one opaque image and reads only the Markdown alt. So for this use case the alt text isn't a nice-to-have on top of the SVG's own a11y; it's the entire accessible surface. There's nothing richer to fall back to.

Which flips how I think about the alt. Honest status:

  • It should describe the data, not the animation. "A rotating planet ringed by contributions" tells a screen-reader user nothing. "GitHub contributions over 53 weeks; heaviest around " is the actual information. My hero/missions/ops alt lean that way; my spectrum and transmissions ones are still lazy ("Languages and tools") and I need to rewrite them to carry the numbers.
  • I have not done a proper screen-reader pass yet (VoiceOver/NVDA) — I was reasoning from the <img> boundary, not testing it. You've convinced me to actually run one before I claim it's accessible.
  • The daily Action regenerates the SVGs, so the alt goes stale unless the generator writes it too. The fix is to emit the alt from the same data that draws each panel, so "heaviest around March" stays true after a rebuild.

So: no clever SVG-internals trick saves this — the lever is genuinely just richer, data-bearing, auto-generated alt, exactly as you implied.

Have you found a reliable way to test the <img>-embedded case specifically? I can eyeball the alt attribute, but I'd love a way to hear what a screen reader actually announces for a GitHub-rendered README image, rather than assuming.