-
-
Notifications
You must be signed in to change notification settings - Fork 0
How It Works
ContribKit never touches the GitHub API. It reads the public contributions page that GitHub renders at github.com/users/{login}/contributions, parses the cells out of the HTML, builds a calendar grid, and renders it. No token, no OAuth, no private data.
---
config:
look: handDrawn
theme: neutral
---
flowchart TD
request(["Request"])
middleware["Middleware: rate limit + security headers"]
validate["Validate input (Zod + value objects)"]
usecase["Use case: fetchContributions"]
scrape["Fetch GitHub contributions HTML"]
parse["Parse cells (date, level, count)"]
grid["Build 53×7 calendar grid"]
render["Render SVG (palette, shape, background)"]
respond["Response + cache headers"]
request --> middleware --> validate --> usecase --> scrape --> parse --> grid --> render --> respond
-
Middleware runs first on every request. For
/api/*paths it applies per-IP rate limiting via the CloudflareAPI_RATE_LIMITERbinding (keyed onCF-Connecting-IP); over the limit it returns429withRetry-After: 60. On every response it attaches the security headers (CSP,X-Frame-Options: DENY,X-Content-Type-Options: nosniff, COOP/CORP,Referrer-Policy,Permissions-Policy). See Web Application for the full header list. -
Validation parses query/route params with Zod, then constructs domain value objects:
-
parseUsernametrims input and tests it against GitHub's username rules (alphanumeric + single hyphens, 1–39 chars). If aUsernameexists, it is valid. -
parseYearacceptsnull/empty (→ latest rolling year), rejects non-integers, and bounds the year to2005 … currentYear. - Any invalid input becomes a typed
Failurebefore any network call.
-
-
fetchContributions(repo)({ username, year })orchestrates the fetch through the repository interface (a curried use case — the repository is injected via closure). - Fetching requests the public contributions HTML from GitHub with browser-like headers. See Fetching Contributions.
-
Parsing extracts each day's
date,level(0–4, run throughclampLevel), and exactcount(from the linked<tool-tip>) via regex over the HTML. See HTML Parsing. - Grid building maps the parsed days onto a fixed 53×7 (371-cell) grid aligned to week boundaries. See Calendar Grid.
- Rendering turns the grid into an SVG string using the selected palette, shape, and background. See SVG Rendering.
-
Response is returned with cache headers
public, max-age=3600, stale-while-revalidate=86400(the/api/healthendpoint is the exception:no-store).
The /api/contributions and /user/:username.svg routes instantiate the repository and use cases once at module load (not per request), so warm Worker isolates reuse them.
Every function that can fail returns T | Failure — never throws. A Failure is a typed discriminated union created by small constructors (notFound, invalidInput, network, parse) and narrowed with the isFailure guard:
type Failure =
| { kind: "NotFound"; username: string }
| { kind: "InvalidInput"; field: "username" | "year"; message: string }
| { kind: "Network"; status?: number; message: string }
| { kind: "Parse"; message: string };At the HTTP boundary, statusFor and messageFor (in application/http/failure-http.ts) map a Failure to a status code and a user-facing message — the only place that mapping lives:
| Failure | Typical cause | statusFor |
messageFor |
|---|---|---|---|
InvalidInput |
malformed username or year | 400 |
the failure's message
|
NotFound |
GitHub returned 404 for the user | 404 |
"User not found" |
Network |
GitHub unreachable or non-OK status | 502 |
the failure's message
|
Parse |
HTML structure changed, no cells found | 502 |
the failure's message
|
Responses with status >= 500 are logged to Better Stack with the username, failure kind, reason, status, and endpoint (api or svg).
- Architecture — the layers behind this flow
- API Reference — the endpoints that trigger it