<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tobias Andersen (turbolego)</title>
    <description>The latest articles on DEV Community by Tobias Andersen (turbolego) (@turbolego).</description>
    <link>https://dev.to/turbolego</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1497773%2Fb4f6acf0-099d-49eb-a2e1-9e0f5fb668cc.png</url>
      <title>DEV Community: Tobias Andersen (turbolego)</title>
      <link>https://dev.to/turbolego</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/turbolego"/>
    <language>en</language>
    <item>
      <title>I turned a 15-year-old iPad 2 into a Spotify display — and sort of a Winamp player</title>
      <dc:creator>Tobias Andersen (turbolego)</dc:creator>
      <pubDate>Mon, 12 Oct 2026 08:35:21 +0000</pubDate>
      <link>https://dev.to/turbolego/i-turned-a-15-year-old-ipad-2-into-a-spotify-display-and-sort-of-a-winamp-player-1o1a</link>
      <guid>https://dev.to/turbolego/i-turned-a-15-year-old-ipad-2-into-a-spotify-display-and-sort-of-a-winamp-player-1o1a</guid>
      <description>&lt;p&gt;I found a better use for a 15-year-old iPad 2 than letting it sit in a drawer.&lt;/p&gt;

&lt;p&gt;I turned it into a &lt;strong&gt;Spotify now-playing display and remote control&lt;/strong&gt;, using nothing more than its stock Safari browser and a web app.&lt;/p&gt;

&lt;p&gt;And then I got a little carried away.&lt;/p&gt;

&lt;p&gt;It now also has a &lt;strong&gt;Winamp 2.x-style interface, an equalizer, a playlist window and a Milkdrop-style visualizer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuvwrnq4rf2rbzsle0ua8.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuvwrnq4rf2rbzsle0ua8.gif" alt="Winamp Mode on iPad 2" width="420" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The whole project is open source:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/turbolego/iPad2Spotify" rel="noopener noreferrer"&gt;GitHub: turbolego/iPad2Spotify&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The original idea
&lt;/h2&gt;

&lt;p&gt;The iPad 2 was released in 2011.&lt;/p&gt;

&lt;p&gt;Mine is running &lt;strong&gt;iOS 9.3.6&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It obviously isn't going to run modern Spotify apps. The App Store isn't going to save me here, and I didn't want to jailbreak it just to turn it into a dedicated music display.&lt;/p&gt;

&lt;p&gt;But it still has a browser.&lt;/p&gt;

&lt;p&gt;So I started wondering:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Could I use the iPad's old Safari browser as a dedicated Spotify display?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer turned out to be yes.&lt;/p&gt;

&lt;p&gt;The iPad doesn't actually play the Spotify audio. Instead, Spotify plays on another device, while the iPad acts as a remote control and display.&lt;/p&gt;

&lt;p&gt;That turned out to be a much better fit for the hardware.&lt;/p&gt;




&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;The iPad can show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;album artwork&lt;/li&gt;
&lt;li&gt;song title&lt;/li&gt;
&lt;li&gt;artist and album&lt;/li&gt;
&lt;li&gt;playback state&lt;/li&gt;
&lt;li&gt;elapsed and total time&lt;/li&gt;
&lt;li&gt;play/pause&lt;/li&gt;
&lt;li&gt;previous/next track&lt;/li&gt;
&lt;li&gt;a playback timeline&lt;/li&gt;
&lt;li&gt;Spotify artist search&lt;/li&gt;
&lt;li&gt;Spotify playlist search&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also has a minimalist display mode that basically turns the iPad into a dedicated album-art display.&lt;/p&gt;

&lt;p&gt;The page polls Spotify adaptively rather than hammering the API constantly: roughly every 60 seconds when nothing is playing, around every 30 seconds during playback, with an additional check near the end of a track. When the page is hidden, polling stops altogether.&lt;/p&gt;




&lt;h2&gt;
  
  
  The constraint: no modern JavaScript
&lt;/h2&gt;

&lt;p&gt;This was probably the most interesting part of the project.&lt;/p&gt;

&lt;p&gt;The iPad 2's Safari is old.&lt;/p&gt;

&lt;p&gt;Really old.&lt;/p&gt;

&lt;p&gt;So the frontend intentionally avoids modern browser features such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fetch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;ES modules&lt;/li&gt;
&lt;li&gt;promises&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;async&lt;/code&gt; / &lt;code&gt;await&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;let&lt;/code&gt; and &lt;code&gt;const&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;service workers&lt;/li&gt;
&lt;li&gt;modern build tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, the frontend uses &lt;strong&gt;ES5 JavaScript and &lt;code&gt;XMLHttpRequest&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In other words, I had to write a modern web application while pretending it was still 2011.&lt;/p&gt;

&lt;p&gt;That is a surprisingly fun constraint.&lt;/p&gt;




&lt;h2&gt;
  
  
  But Spotify authentication was the real problem
&lt;/h2&gt;

&lt;p&gt;There was another problem.&lt;/p&gt;

&lt;p&gt;The iPad's Home Screen web apps run in a standalone fullscreen mode, but interactive Spotify OAuth doesn't reliably stay inside that old standalone browser context.&lt;/p&gt;

&lt;p&gt;So I couldn't simply put a &lt;strong&gt;"Log in with Spotify"&lt;/strong&gt; button on the iPad and call it a day.&lt;/p&gt;

&lt;p&gt;Instead, I built a pairing flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  It works like this
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open the app in normal Safari on a phone or computer.&lt;/li&gt;
&lt;li&gt;Log in to Spotify.&lt;/li&gt;
&lt;li&gt;Spotify authorization produces a short-lived pairing code.&lt;/li&gt;
&lt;li&gt;Open the app on the iPad.&lt;/li&gt;
&lt;li&gt;Add it to the Home Screen.&lt;/li&gt;
&lt;li&gt;Enter the pairing code.&lt;/li&gt;
&lt;li&gt;The fullscreen iPad app receives its own authenticated session.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The pairing code expires after ten minutes and is deleted after it is claimed.&lt;/p&gt;

&lt;p&gt;This means I can do the complicated OAuth dance somewhere modern, then hand the resulting session to the old iPad.&lt;/p&gt;




&lt;h2&gt;
  
  
  The backend is tiny
&lt;/h2&gt;

&lt;p&gt;The iPad doesn't talk directly to Spotify with a client secret.&lt;/p&gt;

&lt;p&gt;The architecture is essentially:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;iPad 2 Safari
     │
     ▼
Vercel Functions
     │
     ├── Spotify OAuth
     ├── Spotify Web API
     └── encrypted session cookie
             │
             ▼
          Spotify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vercel holds the Spotify Client Secret and handles token refreshes.&lt;/p&gt;

&lt;p&gt;The iPad simply asks the backend for the current playback state.&lt;/p&gt;

&lt;p&gt;Redis is only used for state that actually needs to be shared between requests or browsers, such as the short-lived pairing code, badge records and rate limiting. Playback polling itself doesn't use Redis.&lt;/p&gt;

&lt;p&gt;That was an important optimization because I didn't want an always-on iPad constantly burning through a Redis free tier.&lt;/p&gt;




&lt;h2&gt;
  
  
  Winamp 2.9-ish on an iPad 2
&lt;/h2&gt;

&lt;p&gt;The Winamp interface is based on the classic Winamp 2.x look.&lt;/p&gt;

&lt;p&gt;It has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the main player window&lt;/li&gt;
&lt;li&gt;scrolling track title&lt;/li&gt;
&lt;li&gt;elapsed/remaining time&lt;/li&gt;
&lt;li&gt;playback controls&lt;/li&gt;
&lt;li&gt;previous/play/pause/next&lt;/li&gt;
&lt;li&gt;position bar&lt;/li&gt;
&lt;li&gt;equalizer&lt;/li&gt;
&lt;li&gt;playlist window&lt;/li&gt;
&lt;li&gt;visualizer&lt;/li&gt;
&lt;li&gt;Milkdrop-style window&lt;/li&gt;
&lt;li&gt;draggable windows&lt;/li&gt;
&lt;li&gt;docked window layout&lt;/li&gt;
&lt;li&gt;saved window positions&lt;/li&gt;
&lt;li&gt;custom background&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The UI uses the original Winamp 2.91 base-skin bitmaps, with the sprite coordinates following the Webamp skin mapping. The project converts the original bitmap assets into the PNG sheets used by the old-browser-compatible frontend.&lt;/p&gt;

&lt;p&gt;And the frontend is still plain ES5 JavaScript.&lt;/p&gt;

&lt;p&gt;No Webamp bundle.&lt;/p&gt;

&lt;p&gt;No modern framework.&lt;/p&gt;

&lt;p&gt;No build step required for Winamp Mode.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/shorts/aOBSWgWmSMo" rel="noopener noreferrer"&gt;Demo on YouTube: WinAmp 2.9 on 15 year old iPad 2 (sort of)&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  But there is one big catch
&lt;/h2&gt;

&lt;p&gt;This isn't actually Winamp playing Spotify audio.&lt;/p&gt;

&lt;p&gt;The Spotify Web API gives me playback state and playback commands.&lt;/p&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; give the web page the Spotify audio stream.&lt;/p&gt;

&lt;p&gt;So the iPad cannot listen to the audio and perform a real audio analysis.&lt;/p&gt;

&lt;p&gt;That means the visualizer isn't genuinely audio-reactive.&lt;/p&gt;

&lt;p&gt;Instead, I generate a deterministic pseudo-spectrum based on information such as the track ID, playback position and tempo-like patterns.&lt;/p&gt;

&lt;p&gt;The same moment of the same song therefore produces the same visualization.&lt;/p&gt;

&lt;p&gt;It still looks like a visualizer.&lt;/p&gt;

&lt;p&gt;It just isn't analyzing the actual Spotify audio.&lt;/p&gt;

&lt;p&gt;I think that's an important distinction.&lt;/p&gt;




&lt;h2&gt;
  
  
  And the equalizer doesn't equalize anything
&lt;/h2&gt;

&lt;p&gt;The Winamp equalizer has ten bands and a preamp, including the classic Winamp presets.&lt;/p&gt;

&lt;p&gt;But again, the iPad isn't receiving the audio.&lt;/p&gt;

&lt;p&gt;So the equalizer can't change the Spotify sound.&lt;/p&gt;

&lt;p&gt;Instead, it affects the visualizer.&lt;/p&gt;

&lt;p&gt;In other words:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The equalizer equalizes the pixels, not the music.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which is probably the most technically honest way of describing it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Milkdrop on a 15-year-old iPad
&lt;/h2&gt;

&lt;p&gt;The Milkdrop window is powered by OldMilk.&lt;/p&gt;

&lt;p&gt;It uses WebGL when available and falls back to a 2D canvas.&lt;/p&gt;

&lt;p&gt;While playing it renders at roughly 30 fps, and it stops rendering when paused or hidden.&lt;/p&gt;

&lt;p&gt;Getting something resembling a classic Winamp visualization running inside Safari on an iPad 2 in 2026 is honestly more satisfying than it has any right to be.&lt;/p&gt;

&lt;p&gt;[youtube:aOBSWgWmSMo]&lt;/p&gt;




&lt;h2&gt;
  
  
  It can also put your music on your GitHub profile
&lt;/h2&gt;

&lt;p&gt;Then I added another completely unrelated feature.&lt;/p&gt;

&lt;p&gt;The app can generate a &lt;strong&gt;GitHub README badge&lt;/strong&gt; showing the current or most recently played Spotify track.&lt;/p&gt;

&lt;p&gt;It looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;![Last played on Spotify&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://your-project.vercel.app/api/badge/YOUR-SPOTIFY-ID.svg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;](https://your-project.vercel.app/)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz0nhmd05nxfl9i7qcnrz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz0nhmd05nxfl9i7qcnrz.png" alt=" " width="800" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The badge is generated dynamically as an SVG.&lt;/p&gt;

&lt;p&gt;When GitHub requests it, the backend asks Spotify for the current track. If nothing is playing, it falls back to the most recently played track.&lt;/p&gt;

&lt;p&gt;The album artwork is downloaded and embedded directly into the SVG, so GitHub doesn't have to load another remote image from inside the badge.&lt;/p&gt;

&lt;p&gt;And the response is cached for five minutes, so constantly viewing a GitHub profile doesn't result in a Spotify API request every time.&lt;/p&gt;




&lt;h2&gt;
  
  
  The whole thing runs as a web app
&lt;/h2&gt;

&lt;p&gt;That's probably my favourite part of this project.&lt;/p&gt;

&lt;p&gt;There is no iPad app.&lt;/p&gt;

&lt;p&gt;There is no App Store package.&lt;/p&gt;

&lt;p&gt;There is no jailbreak.&lt;/p&gt;

&lt;p&gt;There is no native code running on the iPad.&lt;/p&gt;

&lt;p&gt;It's basically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             ┌─────────────────┐
             │    Spotify      │
             └────────┬────────┘
                      │
                Spotify Web API
                      │
             ┌────────▼────────┐
             │ Vercel Functions│
             │                 │
             │ OAuth           │
             │ Sessions        │
             │ Playback API    │
             │ GitHub badge    │
             └────────┬────────┘
                      │
                 HTTP / JSON
                      │
             ┌────────▼────────┐
             │    iPad 2       │
             │                 │
             │ Safari / iOS 9  │
             │ ES5 JavaScript  │
             └─────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The iPad is basically just a very old web client.&lt;/p&gt;

&lt;p&gt;And that's enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why not just buy a cheap modern tablet?
&lt;/h2&gt;

&lt;p&gt;Because that isn't the point.&lt;/p&gt;

&lt;p&gt;I could obviously buy a modern tablet and install Spotify.&lt;/p&gt;

&lt;p&gt;But there is something satisfying about taking hardware that most people would consider obsolete and finding a job it is still perfectly capable of doing.&lt;/p&gt;

&lt;p&gt;The iPad 2 has a perfectly usable screen.&lt;/p&gt;

&lt;p&gt;It has Wi-Fi.&lt;/p&gt;

&lt;p&gt;It has a browser.&lt;/p&gt;

&lt;p&gt;It can render HTML, CSS and JavaScript.&lt;/p&gt;

&lt;p&gt;That's enough to turn it into a dedicated little music dashboard.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;The project started as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Can I make an old iPad show what Spotify is playing?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It turned into a collection of interesting constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do you authenticate Spotify from an iOS 9 Home Screen web app?&lt;/li&gt;
&lt;li&gt;How do you transfer an authenticated session between browsers?&lt;/li&gt;
&lt;li&gt;How do you write a modern application using ES5?&lt;/li&gt;
&lt;li&gt;How much of a 2010-era UI can you reproduce with web technologies?&lt;/li&gt;
&lt;li&gt;How do you make a Spotify visualizer when you can't access the audio?&lt;/li&gt;
&lt;li&gt;How much can you do with a browser on a 15-year-old tablet?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And perhaps most importantly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Old hardware doesn't necessarily need modern software to be useful.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sometimes it just needs a sufficiently weird project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The project is open source and available on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/turbolego/iPad2Spotify" rel="noopener noreferrer"&gt;turbolego/iPad2Spotify&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The repository contains the frontend, Vercel Functions, Winamp Mode, visualizer, badge generation and deployment instructions.&lt;/p&gt;

&lt;p&gt;If you have an old iPad lying around, I'm curious:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What would you turn a 15-year-old iPad into?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And if you try this project, I'd especially like to see what you do with the Winamp mode. 😄&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: this is an independent hobby project and is not affiliated with or endorsed by Spotify or Winamp. Spotify playback control also generally requires a Spotify Premium account and an active controllable device.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>oldhardware</category>
      <category>retro</category>
      <category>spotify</category>
    </item>
    <item>
      <title>GitHub tells me how much I contributed. I wanted to know where.</title>
      <dc:creator>Tobias Andersen (turbolego)</dc:creator>
      <pubDate>Tue, 06 Oct 2026 13:59:39 +0000</pubDate>
      <link>https://dev.to/turbolego/i-made-a-github-profile-badge-to-show-where-on-earth-you-have-contributed-to-public-repositories-43g5</link>
      <guid>https://dev.to/turbolego/i-made-a-github-profile-badge-to-show-where-on-earth-you-have-contributed-to-public-repositories-43g5</guid>
      <description>&lt;p&gt;GitHub tells me how much I contributed. I wanted to know where.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://turbolego.github.io/github-contrib-globe-badge/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8gw1vm301nyhdbpx2fzf.gif" alt="My contributions badge" width="520" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the badge. Click it to open the interactive 3D globe. The animated GIF rotates on its own so it works as a static profile badge too.&lt;/p&gt;

&lt;p&gt;I built a GitHub profile badge that traces contributions to public repositories back to their owners and maps them onto an interactive globe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;Want your own globe? It takes about two minutes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fork the repository: &lt;a href="https://github.com/turbolego/github-contrib-globe-badge" rel="noopener noreferrer"&gt;https://github.com/turbolego/github-contrib-globe-badge&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Fork the repository, then the workflow will automatically use your GitHub username to generate the badge. No changes to the workflow are needed.&lt;/li&gt;
&lt;li&gt;Enable GitHub Pages → Source: Deploy from branch → &lt;code&gt;main&lt;/code&gt; / &lt;code&gt;(root)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add this to your profile README:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;![My contributions badge&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://raw.githubusercontent.com/turbolego/github-contrib-globe-badge/main/badge.gif&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;](https://turbolego.github.io/github-contrib-globe-badge/)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;turbolego&lt;/code&gt; with your username. GitHub Actions regenerates the badge daily.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/turbolego/github-contrib-globe-badge" rel="noopener noreferrer"&gt;https://github.com/turbolego/github-contrib-globe-badge&lt;/a&gt;&lt;br&gt;
Interactive demo: &lt;a href="https://turbolego.github.io/github-contrib-globe-badge/" rel="noopener noreferrer"&gt;https://turbolego.github.io/github-contrib-globe-badge/&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  How does it work?
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub commits authored by me
         │
         ├── Co-authored-by trailers
         │
         ▼
Resolve true repository origin
         │
         ├── GitHub fork source → follow to ultimate origin
         │
         └── Commits-search fallback → find oldest repo sharing the SHA
         │
         ▼
Repository owner (not the fork owner)
         │
         ▼
GitHub profile location (free-text string)
         │
         ▼
Nominatim geocoding
         │
         ├── Country polygon lookup (primary)
         │
         └── Reverse geocode fallback (coastal / edge cases)
         │
         ▼
Country code + flag
         │
         ▼
┌─────────────────────┬─────────────────────┐
│ badge.gif           │ data.json           │
│ (animated GIF)      │ (Cobe globe data)   │
└─────────┬───────────┴─────────┬───────────┘
          ▼                     ▼
    GitHub README          GitHub Pages
    (static badge)         (interactive Cobe globe)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The entire thing runs on GitHub. No server, no database, no backend API that has to stay running. A daily GitHub Action collects contributions, resolves repository origins, geocodes owners, generates the GIF and JSON, and GitHub Pages serves the interactive globe from the committed &lt;code&gt;data.json&lt;/code&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  The surprisingly hard part: where did the contribution actually happen?
&lt;/h2&gt;

&lt;p&gt;The visual is fun, but the engineering is the interesting part.&lt;/p&gt;

&lt;p&gt;A contribution might exist in a fork, but you don't necessarily want to attribute it to the person who owns the fork.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your fork → someone else's repository → original repository → repository owner → owner's location → country on the globe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project has to resolve the &lt;strong&gt;true origin repository&lt;/strong&gt; for each commit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Follow GitHub's fork &lt;code&gt;source&lt;/code&gt; field&lt;/strong&gt; — if the repo is a registered GitHub fork, walk the fork chain to the ultimate origin.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fall back to commits-search origin detection&lt;/strong&gt; — when a repository duplicates commit history without being a registered GitHub fork (common when someone seeds a new repo from a full clone of another repo's history), search all public repositories that contain the commit SHA and pick the oldest by creation date.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cache origins per repository&lt;/strong&gt; — this runs at most once per distinct repository, not once per commit. The cache is persisted across Action runs with a 30-day TTL (a repo that was private can later become public and turn out to be the true older origin).&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified: the core origin resolution logic&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;fullName&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="c1"&gt;// 1. Search all repos containing this commit SHA&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;searchCommits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`sha:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is:public`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// 2. Expand fork networks&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getRepositoryInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;fork&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// 3. Pick oldest by creation date&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;oldest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getRepositoryInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;oldest&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;oldestDate&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;oldest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;oldest&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;fullName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What counts as a contribution?
&lt;/h2&gt;

&lt;p&gt;A simple GitHub commit search isn't quite enough. Collaborative commits can contain &lt;code&gt;Co-authored-by:&lt;/code&gt; trailers, so I also look for those.&lt;/p&gt;

&lt;p&gt;The generator searches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authored commits&lt;/strong&gt;: &lt;code&gt;author:me is:public -user:me&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Co-authored commits&lt;/strong&gt;: full-text search for &lt;code&gt;"Co-authored-by: me"&lt;/code&gt; then verifies each hit against the actual trailers in the commit message (matching by GitHub noreply address, public profile email, or login name)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This catches contributions to repositories where you weren't the primary author but were credited in the commit message.&lt;/p&gt;




&lt;h2&gt;
  
  
  Turning a GitHub profile location into a country
&lt;/h2&gt;

&lt;p&gt;GitHub's profile location is free-form user-entered text. That's not a structured address.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Oslo, Norway
Berlin
California
Internet
Earth
Remote
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pipeline handles this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Nominatim forward geocode&lt;/strong&gt; — convert the string to lat/lon&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Country polygon lookup&lt;/strong&gt; — test the point against Natural Earth country polygons (the &lt;code&gt;geo-countries&lt;/code&gt; dataset)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverse geocode fallback&lt;/strong&gt; — if the point falls outside any polygon (coastal locations, small islands), call Nominatim reverse geocode with &lt;code&gt;zoom=3&lt;/code&gt; to get the country code from the address details&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flags&lt;/strong&gt; — from the MIT-licensed &lt;code&gt;flag-icons&lt;/code&gt; library for the animated GIF
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Country lookup with fallback&lt;/span&gt;
&lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;countryCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;countryCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;geojson&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;countryCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// wait for Nominatim rate limit&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;countryCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;reverseCountryCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why GitHub Actions?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you'd normally need&lt;/th&gt;
&lt;th&gt;This project&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Database for caching&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;commit-cache.json&lt;/code&gt; committed to repo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server for geocoding&lt;/td&gt;
&lt;td&gt;Calls Nominatim directly from the Action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend API&lt;/td&gt;
&lt;td&gt;GitHub Actions does all computation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cron job&lt;/td&gt;
&lt;td&gt;GitHub Actions scheduled workflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting for globe&lt;/td&gt;
&lt;td&gt;GitHub Pages serves &lt;code&gt;index.html&lt;/code&gt; + &lt;code&gt;data.json&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSL, domain, DNS&lt;/td&gt;
&lt;td&gt;Free on &lt;code&gt;username.github.io&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Action runs daily at midnight UTC, commits &lt;code&gt;badge.gif&lt;/code&gt; + &lt;code&gt;data.json&lt;/code&gt; + &lt;code&gt;commit-cache.json&lt;/code&gt; back to &lt;code&gt;main&lt;/code&gt;, and Pages picks up the new data automatically. Zero infrastructure to maintain.&lt;/p&gt;




&lt;h2&gt;
  
  
  What my globe looks like
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Country&lt;/th&gt;
&lt;th&gt;Commits&lt;/th&gt;
&lt;th&gt;PRs&lt;/th&gt;
&lt;th&gt;Tracked %&lt;/th&gt;
&lt;th&gt;Repositories&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🇳🇴 Norway&lt;/td&gt;
&lt;td&gt;119&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;63.7%&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇧🇷 Brazil&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;9.3%&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 USA&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3.1%&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇵🇹 Portugal&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;4.7%&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇯🇴 Jordan&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1.0%&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇲🇪 Montenegro&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.5%&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇮🇳 India&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1.0%&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇨🇳 China&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.5%&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Total tracked: 137 commits, 25 merged PRs across 8 countries.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Limitation&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitHub locations aren't authoritative&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Self-entered free text. "Internet", "Earth", "Remote" don't geocode.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Not every owner has a location&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Org accounts, bots, and users without a location set are invisible.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Repository owner ≠ all contributors&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You're visualizing repository-owner geography, not the geography of every person who touched the code.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Country attribution is approximate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Especially when profile locations are vague ("California" → US, but which state?)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;History is bounded&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Currently queries from account creation date (fallback &lt;code&gt;2023-01-01&lt;/code&gt;). Very old contributions may be missed if the account is older than the search API can reliably return.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Percentages are "of tracked contributions"&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If a country shows 42%, that's 42% of the &lt;em&gt;resolvable&lt;/em&gt; contribution set — not 42% of all your GitHub activity.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Why I built this
&lt;/h2&gt;

&lt;p&gt;GitHub's contribution graph tells me &lt;em&gt;how much&lt;/em&gt; I've contributed.&lt;/p&gt;

&lt;p&gt;I started wondering about a different question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who are all these contributions actually reaching?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A contribution to an open-source project isn't just an entry on my GitHub graph. It's a connection to another developer, organization, or community somewhere else in the world.&lt;/p&gt;

&lt;p&gt;I wanted to visualize that network — not as an abstract chart, but as a literal map. Where does my open-source work actually end up?&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;Things I'd like to explore (no promises):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Historical animation — scrub through time to watch the globe grow&lt;/li&gt;
&lt;li&gt;Repository-level markers — not just country aggregates&lt;/li&gt;
&lt;li&gt;Organization support — attribute contributions to orgs, not just owners&lt;/li&gt;
&lt;li&gt;Better ambiguous-location handling — disambiguate "Springfield" etc.&lt;/li&gt;
&lt;li&gt;City-level view — zoom in on dense regions&lt;/li&gt;
&lt;li&gt;Contribution types beyond commits/PRs — issues, reviews, discussions&lt;/li&gt;
&lt;li&gt;Interactive repository filtering — click a country, see its repos&lt;/li&gt;
&lt;li&gt;Time-range selection — "last year", "all time", custom range&lt;/li&gt;
&lt;li&gt;Caching/API optimization — smarter search query batching&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What does your globe look like?
&lt;/h2&gt;

&lt;p&gt;Fork the project, add the badge to your profile, and see what your globe looks like.&lt;/p&gt;

&lt;p&gt;I'm especially curious to see whether anyone gets a more geographically distributed contribution map than mine.&lt;/p&gt;

&lt;p&gt;If you find a weird location that the geocoder gets wrong, open an issue or let me know in the comments.&lt;/p&gt;

&lt;p&gt;Where does your open-source work take you?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;GitHub repository: &lt;a href="https://github.com/turbolego/github-contrib-globe-badge" rel="noopener noreferrer"&gt;https://github.com/turbolego/github-contrib-globe-badge&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Interactive demo: &lt;a href="https://turbolego.github.io/github-contrib-globe-badge/" rel="noopener noreferrer"&gt;https://turbolego.github.io/github-contrib-globe-badge/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>github</category>
      <category>javascript</category>
      <category>webgl</category>
    </item>
  </channel>
</rss>
