I wanted to see what I'm actually shipping, not just that...
I wanted to see what I'm actually shipping, not just that I'm shipping. Built a GitHub activity dashboard backed by live GitHub API data at the edge, with two very different...
I wanted to see what I'm actually shipping, not just that I'm shipping. Built a GitHub activity dashboard backed by live GitHub API data at the edge, with two very different chart implementations.
Background
GitHub’s contribution graph is a decent “am I shipping?” signal, but it hides the interesting part: what I’m shipping. I wanted a page that shows activity over time, breaks it down by repository, keeps private work visible without leaking repo names, and doesn’t make the homepage pay a big JavaScript tax.
The feature
The /activity page is a stacked area chart of commits per day, split by repo, plus summary stats like streaks and most active repos. The homepage gets a sparkline preview via a Server Island. Both pull live data from the GitHub API, cached at Cloudflare’s edge for 24 hours.
Data pipeline
Both pages use the same fetchActivityData() function and the same edge caching strategy. No manual syncs, no stale data, no resource abuse.
At request time, the function calls GitHub’s GraphQL API directly using a GITHUB_TOKEN, with parallel Promise.all across year-long time windows (GitHub caps contribution queries at one year). It applies a visibility config (github-repos.json) — show, alias, hide, minimum threshold, plus a forceInclude escape hatch — and produces an ActivityData shape with repo-level commits, calendar totals, and private contribution counts merged into a single timeline.
Both pages set CDN-Cache-Control: public, max-age=86400, stale-while-revalidate=604800. Cloudflare’s edge cache absorbs all traffic after the first request. The origin function and the GitHub API each get hit at most once per day per edge location. During revalidation, visitors still get the stale response instantly while the edge fetches a fresh copy in the background. Bots, traffic spikes, crawlers all just get the cached version. No runaway API calls, no compute burn.
If no GITHUB_TOKEN is set, both pages fall back to a static JSON snapshot committed to the repo. A local sync script (npm run sync-activity) regenerates that snapshot using the same GraphQL contribution queries against GitHub with a real GITHUB_TOKEN. The sync script is useful for local development and as a safety net, but it’s no longer the primary data path.
Both pages use the Server Island pattern (server:defer on a prerender=false component). The page shell — nav, header, footer — is static HTML served instantly from the CDN. Only the data-dependent island (stats + chart) fetches from the origin on the first request. Visitors see a skeleton placeholder while the island resolves, then the real data swaps in. On subsequent requests within the cache window, the island response comes from the edge too.
Private contributions (without repo leaks)
Most of my work is in private repos, so a chart that only shows public commits would quietly undercount by a lot. GitHub’s contribution calendar includes private work in its daily totals but won’t attribute it to specific repositories. Both the sync script and the runtime fetcher calculate the gap between the calendar total and the sum of known repo commits each day, then assign the difference to a Private series.
It’s not perfect, but it’s better than a chart that lies by omission.
Dashboard chart
The /activity chart uses Recharts. It earns its weight there: axes, tooltips, interactive legend toggles, time range filtering.
The chart itself is straightforward. The shell around it is less so. Astro + View Transitions means DOM swaps on navigation, so the React chart is wrapped in a Web Component (<activity-chart>) and rendered with client:only="react". The custom element is a no-op shell — its connectedCallback is empty. It exists so the browser has a stable container that persists across View Transitions. client:only handles the React lifecycle.
Both chart components share a useChartTheme hook that reads chart colors from CSS variables and watches for theme changes via a MutationObserver on the data-theme attribute. Without the observer, toggling between light and dark mode would leave the chart stuck in the wrong palette until the next navigation.
Two fixes on top of the Recharts defaults:
- Stable color indexing. Repo colors are pinned to their position in the full sorted list, so toggling visibility doesn’t reshuffle the palette.
- Fade entrance instead of Recharts’ default left-to-right sweep, gated behind
prefers-reduced-motion.
A ChartErrorBoundary wraps the whole thing so a rendering failure shows a clean message instead of crashing the page.
Homepage mini chart
The homepage mini chart originally pulled in all of Recharts (shared chunk ~281KB) for a single <Area> with no axes, no tooltips, no legend. That’s a lot of JS for a sparkline.
The replacement is a direct SVG path with monotone cubic Hermite interpolation (Fritsch-Carlson, about 35 lines). All path updates go through refs instead of React state, so the animation loop doesn’t trigger re-renders or create per-frame garbage for the GC.
On pointer hover, the sparkline morphs into a flowing ocean wave. Four sinusoidal layers with randomized frequencies and phases blend together, and the chart eases between real data and the wave shape over 800ms using cubic easing. Pre-allocated Float64Array buffers keep the per-frame math allocation-free. On pointer leave, the current wave shape freezes in place and eases back to real data, so the transition out is smooth from wherever the wave happens to be.
Hover detection uses pointerenter/pointerleave on the visible card element (not the full-width section), with a per-frame :hover poll as a fallback for edge cases where pointer events get dropped.
On touch devices, the wave works the same way: press and hold triggers pointerenter, which starts the morph. The web component strips the href on non-hover devices so long-press doesn’t open the browser’s native link menu, and CSS disables text selection and callout. Navigation to /activity goes through the “View dashboard” link in the section header instead.
Trade-offs
Both pages refresh daily via edge caching, but depend on a GITHUB_TOKEN at runtime. Without one, they fall back to the committed JSON snapshot, which goes stale between syncs. GitHub’s data has hard edges too: one-year query windows, no private repo attribution. The Private series is inferred from the gap, not reported. Recharts stays on /activity where it earns its weight. The homepage stays lightweight with the raw SVG.