<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en_US"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://ak1.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://ak1.io/" rel="alternate" type="text/html" hreflang="en_US" /><updated>2026-06-22T17:44:41+00:00</updated><id>https://ak1.io/feed.xml</id><title type="html">Akshay Sharma</title><subtitle>Akshay Sharma — software architect, open-source maintainer, and Kotlin Multiplatform developer. Writing on software craft, architecture, product quality, and the small decisions that make software easier to use.</subtitle><author><name>Akshay Sharma</name><email>akshay@redigit.io</email></author><entry><title type="html">statsvg_rs: GitHub Stats Cards I Actually Control</title><link href="https://ak1.io/blog/2026/06/22/statsvg-rs-github-stats-cards/" rel="alternate" type="text/html" title="statsvg_rs: GitHub Stats Cards I Actually Control" /><published>2026-06-22T00:00:00+00:00</published><updated>2026-06-22T00:00:00+00:00</updated><id>https://ak1.io/blog/2026/06/22/statsvg-rs-github-stats-cards</id><content type="html" xml:base="https://ak1.io/blog/2026/06/22/statsvg-rs-github-stats-cards/"><![CDATA[<p>For a while my GitHub profile had a small pile of stats cards on it. One for streaks, one for top languages, one for repo counts. Each came from a different service, each looked slightly different, and none of them could be told to show <em>exactly</em> what I wanted. If I needed a compact card for a project README and a fuller one for my profile page, I was out of luck — the tools gave me one shape and that was that.</p>

<p>So I built <a href="https://github.com/akshay2211/statsvg_rs">statsvg_rs</a>: a single Rust program that renders GitHub stats cards as SVG, with enough knobs that one tool can produce a header-heavy profile card <em>and</em> a stripped-down lifetime-stats card from the same code.</p>

<h2 id="what-it-actually-makes">What it actually makes</h2>

<p>There are two presets, and the difference is the whole point.</p>

<p><strong><code class="language-plaintext highlighter-rouge">profile.svg</code></strong> is the card you drop into a project README, where the reader doesn’t know who you are yet. It leans on identity: avatar, bio, location, a row of last-year stats, a contribution grid, top languages, and pinned repos.</p>

<p><strong><code class="language-plaintext highlighter-rouge">stats.svg</code></strong> is what I call the anti-profile. It’s meant for your profile README — the <code class="language-plaintext highlighter-rouge">username/username</code> repo — where your avatar and bio are already sitting right above it. So it drops all of that and instead shows the things GitHub tends to hide: <strong>lifetime contributions since you joined</strong>, your longest streak, all-time stars and commits, the top repos you’ve contributed to but don’t own, and your single most-starred project as a highlight.</p>

<p>Both are driven by the same flags. Width, theme (<code class="language-plaintext highlighter-rouge">github_dark</code>, <code class="language-plaintext highlighter-rouge">nord</code>, <code class="language-plaintext highlighter-rouge">dracula</code>, <code class="language-plaintext highlighter-rouge">light</code>, <code class="language-plaintext highlighter-rouge">solarized</code>), which sections to show or hide, how many pinned repos, an optional highlight line. If you want a profile card with no contribution grid, that’s one flag. If you want the lifetime variant but with the header back on, that’s one flag too. That flexibility is the feature I couldn’t find anywhere else.</p>

<h2 id="how-it-works">How it works</h2>

<p>The flow is short and boring in a good way, which is what I wanted.</p>

<ol>
  <li><strong>Fetch.</strong> One GraphQL query to GitHub pulls the user, their repos, languages, contribution calendar, pinned items, and contributed-to repos. The lifetime variant fires a few extra queries — more on that below.</li>
  <li><strong>Compute.</strong> From that raw data it derives the numbers: total stars and forks, language percentages by bytes of code, current and longest streak from the calendar, and the last ~18 weeks of the contribution grid.</li>
  <li><strong>Render.</strong> It builds the SVG as a plain string, section by section, top to bottom. There’s no templating engine and no headless browser — just a small builder that keeps a running vertical cursor and writes one section after another.</li>
</ol>

<p>A couple of details I’m quietly happy with. The avatar is fetched and <strong>base64-embedded directly into the SVG</strong>, so the card is fully self-contained — no external image request when someone loads it. And themes are just plain Rust structs; adding a new one is a constant declaration and a single line to register it, no config format to invent.</p>

<p>For the lifetime numbers there’s a wrinkle worth calling out. GitHub’s API only gives you contribution totals for a date range, not a true “since the beginning of time” number. So to get a real lifetime total, statsvg_rs asks for each year from your join date to now — one query per year — and sums them. The per-year requests fan out concurrently so it stays fast even for an account that’s been around a decade.</p>

<h2 id="how-i-deploy-it-and-why">How I deploy it, and why</h2>

<p>Here’s the part I went back and forth on. The project can run as a live HTTP server — there’s an axum server mode and a Dockerfile — but I don’t deploy it that way. I render to static files instead.</p>

<p>A GitHub Action runs on a schedule (every six hours), builds the binary, renders both <code class="language-plaintext highlighter-rouge">profile.svg</code> and <code class="language-plaintext highlighter-rouge">stats.svg</code>, generates a tiny landing page, and publishes the whole thing to GitHub Pages. The cards you embed are just static files sitting on a CDN.</p>

<p>I picked this for two plain reasons:</p>

<ul>
  <li><strong>There’s nothing to keep alive.</strong> No server to pay for, monitor, or restart at 2am. A scheduled job either runs or it doesn’t, and if it doesn’t, the last good card is still sitting there.</li>
  <li><strong>It’s faster and more reliable for whoever’s looking at it.</strong> A static SVG from a CDN always loads instantly. A live server has cold starts, can go down, and gets hit on every single README view — which is exactly how the shared instances of other stats tools end up rate-limited and broken.</li>
</ul>

<p>Rendering on a schedule means GitHub’s API gets called a handful of times a day on my terms, not once per page view by every visitor. The server mode still earns its keep, though — it’s how I iterate on layout locally. <code class="language-plaintext highlighter-rouge">cargo run</code>, hit <code class="language-plaintext highlighter-rouge">localhost:3000</code> with different query params, and watch the card change without waiting on a full render-and-deploy cycle.</p>

<h2 id="how-you-can-use-it">How you can use it</h2>

<p>If you want your own copy, it’s a fork-and-edit job:</p>

<ol>
  <li>Fork the repo.</li>
  <li>Open <code class="language-plaintext highlighter-rouge">.github/workflows/render.yml</code> and set <code class="language-plaintext highlighter-rouge">STATSVG_USER</code> to your GitHub login (and a theme/width if you like).</li>
  <li>Turn on GitHub Pages with the source set to <strong>GitHub Actions</strong>.</li>
  <li>Push. Your cards publish to <code class="language-plaintext highlighter-rouge">https://&lt;you&gt;.github.io/&lt;repo&gt;/profile.svg</code> and <code class="language-plaintext highlighter-rouge">/stats.svg</code>.</li>
</ol>

<p>If you want private-repo data counted, generate a classic token with <code class="language-plaintext highlighter-rouge">repo</code> scope and add it as a repo secret — otherwise it just uses public data. Then embed whichever card fits where you’re putting it.</p>

<p>That last part is how I run it myself: the <strong>stats card lives on my profile README at <code class="language-plaintext highlighter-rouge">akshay2211/akshay2211</code></strong>, and the <strong>profile card sits in the README of my <a href="https://github.com/akshay2211/DrawBox">DrawBox</a> project</strong>. Same tool, two genuinely different cards, each tuned for where it’s shown.</p>

<h2 id="what-was-actually-hard">What was actually hard</h2>

<p>Honestly? Not much fought me. The mechanics — GraphQL, the SVG building, the Actions pipeline — mostly just worked once they were wired up. The real work wasn’t debugging, it was <em>deciding</em>: what belongs on a profile card versus a stats card, what’s noise, what GitHub already shows the viewer so I shouldn’t repeat it. The anti-profile idea came out of that question, not out of any technical struggle.</p>

<p>The few things I had to design <em>around</em> rather than fight were all just realities of the platform. Lifetime totals needing a query per year, as mentioned. GitHub’s image proxy caching embedded SVGs, which is part of why re-rendering every six hours (rather than chasing real-time) is the right cadence — and why there’s a <code class="language-plaintext highlighter-rouge">?v=</code> cache-bust trick in the README for when you want a card refreshed immediately. And the layout being hand-tracked rather than handed to a layout engine, which is more arithmetic but also means there’s no surprise dependency between me and the pixels.</p>

<p>If you’ve got a wall of mismatched cards on your profile and you’ve ever wished one of them did something slightly different, that’s the itch this scratches. The code is on <a href="https://github.com/akshay2211/statsvg_rs">GitHub</a> — fork it, point it at your username, and make it show what you actually want.</p>]]></content><author><name>Akshay Sharma</name></author><category term="rust" /><category term="github" /><category term="side project" /><category term="svg" /><summary type="html"><![CDATA[Why I built my own GitHub stats card renderer in Rust, how it works, and how it publishes itself as static SVGs every six hours.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ak1.io/img/blog/statsvg-rs.svg" /><media:content medium="image" url="https://ak1.io/img/blog/statsvg-rs.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Shipping Small, Useful Improvements</title><link href="https://ak1.io/blog/2026/06/21/shipping-small-useful-improvements/" rel="alternate" type="text/html" title="Shipping Small, Useful Improvements" /><published>2026-06-21T00:00:00+00:00</published><updated>2026-06-21T00:00:00+00:00</updated><id>https://ak1.io/blog/2026/06/21/shipping-small-useful-improvements</id><content type="html" xml:base="https://ak1.io/blog/2026/06/21/shipping-small-useful-improvements/"><![CDATA[<p>Good software work is often less about making a large dramatic change and more about choosing the smallest improvement that makes the product clearer, faster, or more reliable.</p>

<p>That kind of change is easier to review, easier to test, and easier to explain. It also keeps momentum healthy because every release has a visible reason to exist.</p>

<h2 id="start-with-the-user-path">Start with the user path</h2>

<p>Before touching implementation details, I like to ask one simple question: what should become easier after this change?</p>

<p>If the answer is clear, the scope usually becomes clear too. A useful improvement should remove friction from a real path rather than add surface area just because the system can support it.</p>

<h2 id="keep-the-code-honest">Keep the code honest</h2>

<p>Small changes still deserve care. The implementation should match the existing shape of the codebase, avoid surprising abstractions, and leave the next change easier than this one.</p>

<p>When the code and the user path point in the same direction, shipping becomes less noisy.</p>]]></content><author><name>Akshay Sharma</name></author><category term="software craft" /><category term="product" /><category term="shipping" /><category term="architecture" /><summary type="html"><![CDATA[A short note on keeping product work focused, practical, and easy to validate.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ak1.io/img/blog/shipping-small-useful-improvements.svg" /><media:content medium="image" url="https://ak1.io/img/blog/shipping-small-useful-improvements.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>