Worldwide, country and city leaderboards for the most active developers on GitHub — with the contribution heatmaps, distributions and rank movement that a table of follower counts cannot show.
The existing ranking sites in this space — committers.top, gitstar-ranking, gitista, starfolio — all solve the same problem the same way: a paginated, essentially unstyled HTML table of logins and one number. They work. They also throw away every interesting property of the underlying data — the shape of a year of commits, how a rank moved between snapshots, how contributions distribute against followers, where the activity actually is on a map.
Commitgraph does two things differently:
- It visualises the data. Every ranking has a chart behind it: a 371-day contribution heatmap per profile, a bump chart of rank movement, a tile-grid map of per-country counts, follower/contribution scatter, streak rings, language splits.
- It says what it does not know. Free-text location parsing, private-contribution
counts that GitHub exposes only as a total, calendars that are estimated rather
than fetched, accounts excluded as automation — all of it is labelled in the UI and
explained on
/methodology. Every other site in this category presents its ranking as fact.
pnpm install
pnpm seed # builds a bootstrap snapshot into data/ (requires network access)
pnpm dev # http://localhost:3000pnpm seed is only needed on a clone with an empty data/. A snapshot is committed
to this repository, so pnpm install && pnpm dev is usually enough.
Requires Node 22+ and pnpm 10. No GitHub token is needed to run the site — see below.
There are two ways data/ gets written, and the manifest records which one produced
the current snapshot.
Bootstrap seeder — scripts/bootstrap-seed.ts, run by pnpm seed. It maps the
public gayanvoice/top-github-users
dataset into this project's schema so a fresh clone renders real rankings immediately.
It writes source: "bootstrap" into the manifest, and the site surfaces that on
/methodology with attribution. It fetches 75 users per country, keeps a city only
where at least 8 tracked developers agree on the name, and generates profile pages for
the worldwide top 500 plus each place's leaders.
Scheduled crawler — scripts/crawler/, run by pnpm crawl. This is the project's
intended long-term source: it talks to the GitHub API directly, fetches real 371-day
contribution calendars and language breakdowns rather than estimating them, and
replaces the bootstrap snapshot wholesale on its first successful run. It is not in
this tree yet — the crawl and crawl:fixtures scripts in package.json reference
scripts/crawler/index.ts, which has not been committed. Today the seeder is the only
working source.
Where snapshots live — everything under data/, committed as JSON so any ranking
can be diffed against the day before it:
| Path | Contents |
|---|---|
data/manifest.json |
Snapshot date, source, counts, totals, and the country/city indexes |
data/leaderboard/worldwide.json |
The global ranking |
data/country/{id}.json |
Per-country leaderboards (88 at present) |
data/city/{id}.json |
Per-city leaderboards (119 at present) |
data/user/{login}.json |
Full per-developer records, including the calendar (451 at present) |
data/history/worldwide.json |
Compacted rank history — what the bump chart reads |
data/org/top.json, data/repo/top.json |
Organization and repository boards |
data/flagged.json |
Accounts excluded as automation, published rather than hidden |
The site is fully static. lib/data.ts reads these files from disk at build time
and validates each one against a zod schema, so a malformed crawl commit fails the
build instead of rendering nonsense. Every dynamic route (/u/[login],
/countries/[id], /cities/[id]) has generateStaticParams. There is no request-time
GitHub call and no runtime GitHub token anywhere in the app — a token is only ever
needed by the crawler, offline.
- Next.js 16, App Router. React 19 server components by default; the
"use client"boundary sits at the individual motion pattern, not at the page. - Tailwind v4, consuming the design-token block at the top of
app/globals.cssthrough@theme inline. Tokens resolve at build time, so anything injected at runtime via astyleattribute usesvar()directly rather than a utility class. components/patterns/— one component per motion pattern, 18 of them, plus three shared hooks (use-in-view,use-raf,use-reduced-motion). Each file documents the obligations it honours in a header comment.components/charts/— seven hand-rolled SVG charts (bump-chart,heatmap,scatter,sparkline,split-bar,streak-ring,tile-grid-map). No charting library;d3-scale,d3-shapeandd3-arrayare used for maths only, and the markup is ours. They render on the server, which is what keeps the no-JS contract achievable.components/background/— a four-layer composite: a static radial base wash (also the reduced-motion and no-WebGL fallback), a WebGL2 aurora mesh at half DPR and 30fps that pauses off-screen, a canvas commit field seeded from the real contribution distribution, an inline SVG grain tile, and a CSS hairline grid aligned to the editorial column grid. The whole thing isaria-hidden, fixed andpointer-events-none.lib/—data.ts(loaders),schema.ts(zod mirrors oftypes.ts),calendar.ts(deterministic calendar estimation),format.ts,geo.ts(country centroids for the tile map),motion.ts(easing/duration constants mirroring the CSS tokens).
The snapshot is not only a website. The same data is served three ways, all from one shared
query layer (lib/api/queries.ts) so the surfaces cannot drift apart.
A Model Context Protocol server over the snapshot, living inside the Next app, so deploying the site deploys the server.
claude mcp add --transport http commitgraph https://<domain>/api/mcp
JSON-RPC 2.0, protocol 2025-06-18, no authentication, GET returns 405. It is hand-rolled
(lib/mcp/) rather than built on @modelcontextprotocol/sdk: the SDK's HTTP transport expects
Node req/res rather than the Web Request/Response a route handler receives, and its
released versions pin zod 3 while this project runs zod 4. Zod 4's z.toJSONSchema() generates
each tool's advertised inputSchema from the very schema that validates the call, so the two
cannot disagree.
Twelve tools. Start with commitgraph_describe_dataset — it returns the snapshot date, every
count, the meaning and provenance of every field, and the full list of valid scope ids. Then
commitgraph_search_developers, commitgraph_get_developer, commitgraph_get_leaderboard,
commitgraph_compare_developers, commitgraph_list_places, commitgraph_get_organizations,
commitgraph_get_repositories, commitgraph_get_rank_history, commitgraph_get_statistics,
commitgraph_get_flagged_accounts, and commitgraph_get_integration_guide.
Four resources: commitgraph://policy, commitgraph://manifest, commitgraph://schema (JSON
Schema generated from lib/schema.ts) and commitgraph://leaderboard/worldwide.
The same data for clients that do not speak MCP. /api/v1 describes itself; /api/openapi.json
is the OpenAPI 3.1 document.
/.well-known/mcp.json points at the MCP endpoint and names its tools; /llms.txt orients an
agent in plain text, caveats included.
Search has to see every ranked developer, but they live across 88 country files and 119 city
files. scripts/build-index.ts flattens them into data/index/search.json — one read instead
of 207. It runs automatically as prebuild.
Motion patterns come from the public rejouice-patterns MCP server at
https://rejouice-patterns.vercel.app/api/mcp, registered in .mcp.json.
The token block at the top of app/globals.css is fetched verbatim from
rejouice://tokens and is never edited in place — it is overridden in a second :root
block immediately below it.
That override block is the important design decision here. The corpus defaults are
scaled down deliberately. They are agency-scale: a 14rem display face is correct for
a studio site with six words on the screen. This is a data site, so --type-display
drops to clamp(2rem, 5.5vw, 5.5rem) — roughly a third of the corpus default — along
with proportional cuts to --type-h1, --type-h2 and --space-xl. Dialling the
display down is what lets density, tables and charts carry the page instead of
headlines. The dark/light pair is swapped rather than hard-coded, so
scroll-color-inversion can flip a section without new literals.
Two rules govern how patterns are spent:
- A budget of at most four motion patterns per route. Chrome patterns in the root
layout (
condensing-sticky-nav,curtain-route-transition,contextual-cursor,numeric-preloader) are counted once, globally, not against every page. rejouice_review_pattern_stack— the MCP server's review tool — validates each route's stack against the corpus before that route is considered done. It is what catches a page that has quietly accumulated five patterns, or a combination the corpus warns against.
Two rules are non-negotiable and are enforced by a test, not by intent:
- Every route is readable with JavaScript disabled. The rankings, charts and copy are server-rendered; motion patterns only ever remove a transform or raise an opacity, so a pattern that never hydrates leaves content visible rather than hidden.
- Nothing is left translated, faded or hidden under
prefers-reduced-motion: reduce. Components branch in JS;app/globals.csscarries a global backstop that forces[data-reveal]to its final state so a component that forgets is still correct.
Both are verified by:
pnpm build && pnpm start # in one shell
node tests/shots.mjs # in anothertests/shots.mjs drives Playwright over all eleven routes at desktop and mobile, with
reduced motion on and off and with JavaScript disabled. It fails the run if any element
is left below 5% opacity or heavily translated under reduced motion, if a no-JS page
renders under 200 characters of text in <main>, or if any page logs a console error.
Screenshots land in /tmp/shots by default.
Beyond that: a skip link, a 3:1 focus ring that swaps colour when a section inverts,
44px touch targets in the condensed nav, tabular-nums on every number so odometers do
not reflow, and DOM order that always equals reading order — the asymmetric grid never
reorders content.
The short version; the full account is on /methodology.
- Measured vs estimated. Contribution totals and follower counts are measured.
Where the day-by-day calendar has not been fetched, the heatmap shape is a
deterministic estimate derived from the measured total: the same login always
produces the same calendar, and the total always matches exactly. Every field carries
a
Provenanceofmeasured,estimatedorunavailable(lib/types.ts), and anything estimated is labelled in the UI. - Automation exclusion. Accounts above 300,000 contributions in twelve months —
over 820 every day without a break — are flagged as automation and excluded from every
ranking. That threshold is set high enough to keep genuine high-volume maintainers who
run packaging pipelines under their own name. The excluded accounts are listed openly
on
/methodologyand indata/flagged.jsonrather than being silently dropped, which is the part most leaderboards get wrong: they either leave a bot at number one or remove it without telling you. - Free-text location parsing. GitHub has no structured country or city field, only a line people fill in however they like. Country comes from searching that field; city is parsed from the same string and kept only where at least eight tracked developers agree on a name. "Earth", "remote" and "/dev/null" are discarded rather than guessed at. This is the largest source of error on the site.
- Organizations are ranked by the combined contributions of tracked developers who name them in a free-text company field, so the board reflects what people type, spelling variants included.
| Script | Command | What it does |
|---|---|---|
dev |
next dev |
Development server |
build |
next build |
Production build; reads and validates data/ |
start |
next start |
Serves the production build |
lint |
eslint . |
Flat config in eslint.config.mjs (eslint-config-next) |
typecheck |
tsc --noEmit |
TypeScript, strict |
crawl |
tsx scripts/crawler/index.ts |
Scheduled GitHub crawler — not yet committed |
crawl:fixtures |
tsx scripts/crawler/index.ts --fixtures |
Crawler against recorded fixtures — not yet committed |
seed |
tsx scripts/bootstrap-seed.ts |
Builds the bootstrap snapshot into data/ |
test |
node --test "scripts/**/*.test.ts" |
Unit tests for the pipeline — no test files yet |
The Playwright sweep is not in package.json; run it directly with node tests/shots.mjs.
The bootstrap snapshot is derived from gayanvoice/top-github-users, with thanks. Design tokens and motion patterns come from rejouice-patterns.