My portfolio said I hadn't pushed in nine weeks
My portfolio hero renders live GitHub stats. In July it claimed my last push was nine weeks ago — right beside “26 commits in the last 30 days”. Both numbers were frozen in May. On build-time data, cadence coupling, and which GitHub API actually tells the truth.
This week I reviewed my own portfolio the way a recruiter would: cold, on a Tuesday, with fifteen seconds of attention. The hero has a stat card that's supposed to be proof of life — last GitHub push, commits in the last thirty days. It read: last push 9w ago, and right next to it, 26 commits · last 30d. Those two numbers cannot both be true. If my last push was nine weeks ago, there were zero commits in the last thirty days. Both came from the same JSON file, and both were frozen in May.
A checked-in JSON file is a cache with no TTL
The setup was reasonable, which is what makes it worth writing down. A prebuild script fetched my GitHub activity and wrote it to a JSON file in the repo. Checked in, so builds are offline-safe and never blocked on a flaky API. Rendered straight into the hero at build time. Every choice defensible in isolation.
{
"user": "PiotrRomanczuk",
"generatedAt": "2026-05-12T11:14:25.498Z",
"lastPush": "2026-05-12T10:54:35Z",
"commitsLast30d": 26
}The failure mode isn't a bug. It's cadence coupling: the data refreshes when the site deploys, and the site deploys when I touch the site. The moment I'm heads-down shipping other projects — which is exactly what the stat is supposed to prove — the portfolio stops deploying and the number starts rotting.
The dashboard was working exactly as built. That was the problem.
Fetch at request time, keep the snapshot as a fallback
The fix took an afternoon. The volatile numbers — last push, 30-day and 7-day counts, repo stats — moved to a server-side fetch with incremental static regeneration. The checked-in snapshot stayed, demoted to a fallback.
export async function getGithubActivity(): Promise<GithubActivity> {
const fallback = fromSnapshot(); // checked-in JSON, refreshed on deploy
try {
const [pushActivity, repoStats] = await Promise.all([
fetchPushActivity(), // fetch(..., { next: { revalidate: 3600 } })
Promise.all(SHOWCASED_REPOS.map(fetchRepoStats)),
]);
return merge(pushActivity, repoStats, fallback);
} catch {
return fallback; // API down ≠ empty hero
}
}The page itself declares export const revalidate = 3600, so the stats regenerate hourly whether or not I ever deploy again. If GitHub is down or rate-limits me, visitors get the snapshot instead of an error. Builds remain offline-safe. And because every number now comes out of the same fetch, the impossible pairing — old push, fresh commits — is structurally unrepresentable.
The second trap: which GitHub API you ask
While fixing this I regenerated the snapshot without an API token, and the contribution heatmap collapsed: 2 active days out of 182. My site went from “stale” to “abandoned” in one command. The culprit was the fallback data source.
The public events endpoint (/users/:user/events/public) only sees public repositories, only reaches back about ninety days, and caps out at three hundred events. Most of my recent work lives in private repos, so the endpoint reported a ghost town. The GraphQL contributions calendar sees everything you can see — private contributions included — but requires a token.
query($login: String!) {
user(login: $login) {
contributionsCollection {
contributionCalendar {
weeks { contributionDays { date contributionCount } }
}
}
}
}With the token: 91 of 182 days active, 191 contributions in the last thirty days — against the 26 the events endpoint reported. Same developer, same month. One API said I was prolific; the other said I was gone.
Numbers that sit next to timestamps
The general rule I took away: every live-looking number on a page makes an implicit freshness promise, and the page inherits the credibility of its stalest number. A contribution graph, a “last updated” badge, a DAU counter — each one is a claim about now. Render it from data that has an actual TTL, or don't render it.
If a number can go stale silently, it will — and it will pick the worst possible audience to do it in front of.
The irony isn't lost on me that the page whose headline promises products that “survive their first users” was itself failing its first users. It regenerates hourly now. The snapshot is just a seatbelt.