Skip to main content

Building a Terminal-Themed GitHub Profile README (With Live, Self-Updating Stats)

Introduction
#

GitHub has a small, under-used feature: create a public repository with the exact same name as your username, and the README.md inside it gets rendered at the top of your profile page — before your pinned repos, before anything else. It’s the closest thing GitHub gives you to a personal landing page, and most people leave it either empty or filled with a stock template.

This post walks through building one styled as a single, continuous Linux terminal session — a whoami, a cat about.md, a skills listing, hoverable icons and all — with genuinely dynamic content underneath: repo counts, per-project stars, contribution streaks, and a visitor counter that update themselves with zero maintenance, no personal access token, and no server of your own.

One important expectation to set upfront: a GitHub profile README is static markdown — GitHub strips out all <script> tags, so you cannot build a literally interactive terminal where typing whoami triggers a real command. What you can build is something that convincingly looks like a terminal session, with the “dynamic” parts being live-updating images, not live-executing code. That distinction matters and is worth understanding before you start.


Step 1: Create the Special Repository
#

  1. Go to github.com/new
  2. Name the repository exactly your username (case-sensitive match)
  3. Set it to Public — private doesn’t render on your profile
  4. Skip the README initialization — you’ll push your own

GitHub confirms it worked with a banner on the empty repo: "your-username/your-username is a special repository."


Step 2: The Trick Behind “Dynamic” Content in Static Markdown
#

Since a README can’t run code, every “live” element here is actually an image — an SVG generated on-the-fly by a small web service every time GitHub (or a browser) requests it. The two techniques used throughout:

shields.io dynamic JSON badges
#

shields.io can fetch a JSON document and render one field from it as a badge, generated fresh on each request (cached briefly):

https://img.shields.io/badge/dynamic/json?url=<json-endpoint>&query=$.<field>&label=<label>&color=<hex>

Pointed at GitHub’s own public API, this gives you a badge that always reflects reality with no code of your own running anywhere:

![Public Repos](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Fusers%2Fyour-username&query=%24.public_repos&label=Public%20Repos&color=39FF14&style=for-the-badge&logo=github)

api.github.com/users/your-username returns a JSON object with public_repos, followers, following, etc. — swap the query field to show any of them. The same trick works per-repository against api.github.com/repos/your-username/your-repo for star count and primary language:

![stars](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Frepos%2Fyour-username%2Fyour-repo&query=%24.stargazers_count&label=stars&color=39FF14&logo=github)

Create a new repo tomorrow, and the public-repo-count badge shows the new number the next time someone loads your profile — no workflow, no secret, no cron job.

Third-party stat-card generators
#

A handful of community projects run a small server that draws an SVG card from your public GitHub activity on each request:

What it showsService
Current/longest contribution streakstreak-stats.demolab.com/?user=your-username
Daily contribution graphgithub-readme-activity-graph.vercel.app/graph?username=your-username
Tech stack icon rowskillicons.dev/icons?i=py,js,docker,...
Typing/rotating headline textreadme-typing-svg.demolab.com/?lines=...
Profile visit counterkomarev.com/ghpvc/?username=your-username

None of these need authentication for public data — you just embed the URL as an <img> and the service does the work.


Step 3: A Gotcha Worth Knowing — Public Instances Go Down
#

While building this, two of the most commonly recommended widgets turned out to be broken: the main stats/top-languages card at github-readme-stats.vercel.app returned DEPLOYMENT_PAUSED, and the trophy case at github-profile-trophy.vercel.app returned Payment required / DEPLOYMENT_DISABLED.

This isn’t a one-off — both projects explicitly warn about it in their own docs:

“The public Vercel instance is best-effort and can be unreliable due to rate limits and traffic spikes.”

These are extremely popular projects (tens of thousands of profiles embed them), so the shared free-tier hosting periodically gets rate-limited or paused entirely. Always test the actual image URL in a browser or with curl before committing it to your README — a broken <img> tag on your professional profile page looks worse than not having the section at all.

curl -s -o /dev/null -w "%{http_code}\n" "https://github-readme-stats.vercel.app/api?username=your-username"
# 503 -> don't ship it

If you want those specific widgets and can’t rely on the shared instance, both projects support deploying your own copy to your personal free Vercel account in a few clicks, which isolates you from everyone else’s traffic:

  • Stats card: github.com/anuraghazra/github-readme-stats → “Deploy on your own”
  • Trophy case: github.com/ryo-ma/github-profile-trophy → “Deploy your own instance”

For this build, I skipped both and leaned on the widgets that were actually confirmed live (streak stats, activity graph, skill icons, shields.io, visitor counter) — a “stunning” profile that’s actually up beats a more elaborate one with broken images.


Step 4: One Continuous Terminal, Not a Stack of Boxes
#

My first pass at this used a bold markdown header above every command (### $ whoami, ### $ cat about.md, a horizontal rule between sections) with each command in its own small fenced code block. It looked fine, but it read as a stack of separate widgets, not one terminal session — which was the actual goal. Fixing that meant answering two questions properly instead of guessing.

What can GitHub’s sanitizer actually keep? (tested, not assumed)
#

My instinct was to wrap everything in a custom-styled <div> — a dark background, a rounded border, a title bar — to fake a single “terminal window” frame around the whole README. That doesn’t work, and you don’t have to take my word for it: GitHub exposes the exact same renderer it uses for READMEs as a public API, so you can check what survives before you commit anything:

curl -s -X POST https://api.github.com/markdown \
  -H "Accept: application/vnd.github+json" \
  -H "Content-Type: application/json" \
  -d '{"text": "<div style=\"border:2px solid #39FF14;border-radius:10px\">test</div>", "mode": "markdown"}'

The response comes back as <div>test</div> — the entire style attribute is silently dropped. Same for the legacy bgcolor attribute. GitHub’s sanitizer strips both, no exceptions, so there is no way to draw a custom box, border, or background color anywhere in a README. align="center" survives (that’s the one presentational attribute GitHub keeps), which is why centering works but coloring a container doesn’t.

Given that, the only thing that visually looks like a “terminal box” at all is GitHub’s own default styling of a fenced ``` code block — dark background, padding, rounded corners, applied automatically, not something you control. And that box has one hard limitation: it can only contain plain text. An ![image](url) or <img> tag inside a fenced code block just prints as literal text — it does not render. This is the actual constraint that shapes everything else in this section.

Merging fences instead of stacking them
#

Since images can’t live inside a code fence, “one continuous terminal” really means: merge every consecutive text-only prompt/output pair into a single shared fence, and only start a new fence where the next thing has to be an image. Three text-based “commands” (whoami, cat about.md, and the ls skills/ prompt line) can share one fence since none of them need an image mid-stream:

```bash
vivek@github:~$ whoami
Vivek U.S.
Industrial IoT (IIoT) Architect & Technology Manager @ Omega Solutions, UK

vivek@github:~$ cat about.md
> IIoT architect who lives at the intersection of embedded firmware...

vivek@github:~$ ls skills/
```

Then the skills image renders directly below (outside the fence), a new one-line fence opens for the next command, and so on. Drop the bold headers, drop the horizontal rules, drop any explanatory asides between sections, and what’s left reads as continuous shell scrollback — prompt, output, prompt, output — all the way down, even though under the hood it’s still six separate fenced blocks stitched together with no visual gap in between them.

Hover tooltips on the skill icons (also tested against the same API)
#

The skills row was originally one combined image — a single request to skillicons.dev returning all icons stitched together as one SVG. That’s compact, but a single <img> tag can only carry one tooltip for the whole image, not one per icon.

The fix: request each icon individually (skillicons.dev/icons?i=py returns just Python, one call per skill) and give each of the resulting <img> tags its own title attribute. Whether that survives GitHub’s sanitizer is again worth testing rather than assuming:

curl -s -X POST https://api.github.com/markdown \
  -H "Accept: application/vnd.github+json" -H "Content-Type: application/json" \
  -d '{"text": "<img src=\"https://skillicons.dev/icons?i=py\" title=\"Python\" alt=\"Python\">", "mode": "markdown"}'

Unlike style, the title attribute comes back untouched. Browsers render title as a native tooltip on hover with zero JavaScript, so 32 individual <img> tags — one per skill, each with its own title — gives a hoverable skills row for free:

<img src="https://skillicons.dev/icons?i=py&theme=dark" title="Python" alt="Python" width="48" height="48" />
<img src="https://skillicons.dev/icons?i=php&theme=dark" title="PHP" alt="PHP" width="48" height="48" />

The trade-off is 32 small HTTP requests instead of 1 — negligible for a README, and worth it for hover context on every icon.

A monochrome-green accent color (#39FF14) on the badges and typing banner reinforces the terminal look without needing any custom CSS — color is pushed through each service’s own color=/theme= query parameter instead, since there’s no <style> block to reach for.


Step 5: Native Pinning, Not Just README Links#

GitHub separately lets you pin up to 6 repositories so they render as cards directly on your profile, above or below the README depending on theme — go to your profile → Customize your pins. This is worth doing in addition to linking projects in the README, since pinned repos show live star/fork counts and a language color bar without any markdown at all.


Step 6: Private Repo Counts (Optional, More Setup)
#

The shields.io trick above only works for public data — api.github.com/users/<you> never reveals private repo counts to an unauthenticated request, by design. If you want a “Public: 12 · Private: 8” style badge, that requires:

  1. A GitHub Personal Access Token with repo scope
  2. Stored as an encrypted Actions secret in your profile repo (never in the README or committed anywhere)
  3. A small scheduled GitHub Action that runs a script authenticated with that token, computes the counts, and rewrites a marked section of README.md, then commits

This is meaningfully more setup than the zero-token approach above, and it’s the only place a secret enters the picture at all — everything else in this build is 100% public data through public endpoints.


Conclusion
#

A GitHub profile README earns its “dynamic portfolio” reputation almost entirely from a handful of small, free, public image-generating services — not from anything running on your own infrastructure. The real engineering judgment is in:

  • Verifying every embedded URL actually resolves before shipping — popular free-tier widgets go down more often than their star counts would suggest
  • Choosing shields.io + GitHub’s own API wherever possible, since both are far more reliable than any third-party clone
  • Testing GitHub’s sanitizer against its own markdown API instead of guessing what HTML/CSS survives — it’s how I found that style and bgcolor get stripped (no custom-styled boxes) but title doesn’t (free hover tooltips)
  • Accepting that “dynamic” here means live-rendered image, not interactive terminal — and designing the terminal illusion around formatting (merged fences, no headers, no dividers) rather than fighting markdown’s lack of JavaScript

The result is a profile page that reads as one continuous shell session, updates its repo counts, star counts, and contribution streaks forever, and shows a tooltip on every skill icon — without a single scheduled job to maintain.

Vivek US
Author
Vivek US
14+ years architecting secure, resilient Industrial IoT platforms — embedded systems, edge computing, mesh networking, and cloud-native infrastructure.