This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
github-boardingpass — Fastify API that generates developer persona cards in boarding-pass style from GitHub data. Users get classified into RPG-like "classes" based on language usage and contribution patterns, then a stylized SVG/PNG card is generated.
Intended use: embed in GitHub profile READMEs, like github-readme-stats.
Live endpoint: GET /api/card?username=<github_username>
- Runtime: Node.js + TypeScript (ESM,
.jsimport extensions required) - Framework: Fastify 5 +
@fastify/cors+@fastify/rate-limit - SVG: Satori (object-tree → SVG)
- PNG:
@resvg/resvg-js(rendered at 2× then downsampled) - Package Manager: pnpm
- Dev Runner:
tsx watch
pnpm dev # tsx watch src/server.ts
pnpm build # tsup, ESM + .d.ts
pnpm start # node dist/server.js
pnpm typecheck # tsc --noEmit (no test suite yet)Quick manual test: curl 'http://localhost:3000/api/card?username=<u>' > out.svg.
PNG: &format=png. Override detected class: &class=Backend%20Knight. Theme: &theme=light.
Request flow:
HTTP → routes/card.ts
→ cardCache.getOrSet(key)
→ fetchGitHubData (cached) → classifyPersona → renderCard
→ buildBoardingPass (Satori object tree) → satori() → SVG post-processing → optional resvg PNG
server.ts— Fastify boot, CORS, rate-limit, route registration,/health,/config.ts— env:PORT,GITHUB_TOKEN,CACHE_TTL_MS(default 600_000),CACHE_MAX_ENTRIES(default 500)cache.ts— generic TTL+LRUcreateCache<T>({ttlMs, max}).getOrSetdedups concurrent calls via in-flight Promise map. LRU touch onget(Map insertion-order = recency)github/fetcher.ts— GraphQL fetch +calculateCurrentStreak.fetchGitHubDatais cached wrapper;fetchGitHubDataRawdoes the network callpersona/classifier.ts— class detection (language scoring + DevOps weight boost + Fullstack/Bard rules), XP, level, stats, titlecard/theme.ts—getTheme(class, mode),getRarity(level), layout constants (CARD_WIDTH/HEIGHT,MAIN_WIDTH,STUB_WIDTH,SPACER_WIDTH)card/primitives.ts—divider,label,val(Satori-tree helpers)card/fonts.ts— TTF loaders, cached at module scopecard/icons.ts— inline SVG icons +svgToDataUrl()for<img src>in Satoricard/svg-fx.ts— raw-SVG post-Satori injection:injectHeaderStripes(SVG only),injectTearLine(mask + dashed line)card/post-process.ts— toggleable filter overlays (vignette, noise, gradient, paper texture, stamp)card/sections/main.ts— left section: header / passenger / stats / footercard/sections/stub.ts— right section: gate, level, class, rarity, XP, stars, commits, PRs, issued date, streakcard/renderer.ts— orchestrator:buildBoardingPasscomposition +renderCard(Satori → svg-fx → post-process → optional resvg)routes/card.ts—/api/card(renders, two-layer cached: GitHub + final card),/api/persona(JSON, GitHub-cache only)
Two separate caches share the same TTL/max config:
- GitHub user data (
github/fetcher.ts) — key =username.toLowerCase(). Shared by/api/cardand/api/persona. - Rendered card (
routes/card.ts) — key =username|format|theme|class. Skips both GitHub fetch and Satori/resvg render.
In-flight dedup means N concurrent requests for the same key produce 1 underlying call.
Two columns split by a 20 px spacer with a tear line + circular notches punched via SVG <mask>:
┌──────────────────────────────────────────────────────┬────────────┐
│ AIRLINE [stripes] BOOKING REF [avatar] │ GATE LV │
├──────────────────────────────────────────────────────┼────────────┤
│ PASSENGER NAME FROM [✈] TO FLIGHT DATE │ CLASS RAR │
│ @user / TITLE 2019 NOW CLASS today ├────────────┤
├──────────────────────────────────────────────────────┤ XP STAR │
│ CODE COLLAB SHIP STACK IMPACT (mini bars) ├────────────┤
├──────────────────────────────────────────────────────┤ CMT PRS │
│ Lang / Lang / Lang barcode ├────────────┤
└──────────────────────────────────────────────────────┤ ISSUED │
^tear line + notches^ │ STREAK │
└────────────┘
renderer.ts picks one of 10 airline names (AIRLINES array) deterministically via a username hash → stable per user → cache-safe. Tagline is BOARDING PASS · {RARITY} CLASS, where rarity is derived from level.
{USERNAME[0..3].toUpperCase().padEnd(3,'X')}-{XP_LAST4}-{CLASS_INITIALS}. Example: JST-3847-OB.
calculateCurrentStreak in github/fetcher.ts: flatten contributionCalendar.weeks descending, skip today if 0 (day not over), count consecutive non-zero days backward.
Stats use a sqrt curve for distribution: score = round(sqrt(min(1, value/max)) * 99), clamped to 1–99. This rewards mid-tier devs visibly while keeping 99 aspirational. Max caps:
| Stat | Source | Max for 99 |
|---|---|---|
| CODE | totalCommits |
3,000 |
| COLLAB | totalReviews |
300 |
| SHIP | totalPRs |
1,000 |
| STACK | contributedTo |
20 |
| IMPACT | totalStars |
2,000 |
XP is linear: commits·2 + PRs·5 + issues·3 + stars·4 + reviews·4 + contributedTo·10 + followers.
Level = min(99, floor(sqrt(XP/10))). Rarity tiers from getRarity(level): COMMON < 20, UNCOMMON < 40, RARE < 60, EPIC < 80, LEGENDARY ≥ 80.
renderCard rewrites the outer <svg> width/height attrs by scale (default 1.35, clamped 0.5–3). viewBox is preserved at CARD_WIDTH × CARD_HEIGHT so content scales without layout recomputation. PNG resvg.fitTo uses CARD_WIDTH * scale * 2 for matching resolution. Route exposes ?scale= and includes it in the cache key.
renderCard in card/renderer.ts applies post-Satori transforms in this exact order:
injectHeaderStripes(SVG only) — appends stripes<image>before</svg>postProcess(vignette overlay etc.) — adds<rect>overlay before</svg>injectTearLine— wraps everything in<g mask="url(#cardMask)">so the notches become real holes
Do not reorder steps 2 and 3. If postProcess runs after injectTearLine, the vignette overlay is drawn outside the mask group → the notch circles get repainted by the overlay and look semi-transparent instead of fully cut out.
Glow drop-shadow was previously applied as a CSS filter on the outer <svg> but removed — the filter's blur halo leaks through the masked notch holes and paints them with the accent color.
- Every
divwith multiple children must havedisplay: flex - No
position: absoluteon children without explicit parent positioning - No
grid, noclip-path, no complex CSS children: ''for empty divs (not[])- Only loaded fonts — no system fonts. Both bundled fonts are static TTF (variable fonts throw
parseFvarAxis) - Emoji needs an emoji font — stick to plain text / ASCII
- Some Unicode (★, →, ║) may not exist in loaded fonts — verify before using
transformnot supported — rotate at SVG path level instead
Effects that can't be done in Satori (stripes, tear-line mask, dashed line, glow animation) are injected as raw SVG strings in card/svg-fx.ts after satori() returns.
PNG path skips one effect (resvg renders at 2× and doesn't preserve the stripe image cleanly):
- Diagonal header stripes (
injectHeaderStripes)
The tear-line mask + post-process overlays apply to both formats.
Static TTF in assets/fonts/:
Rajdhani-Bold.ttf— headers, labels, class names, statsInter-Regular.ttf— body, usernames
Download URL pattern: https://cdn.jsdelivr.net/fontsource/fonts/<name>@latest/latin-<weight>-normal.ttf. Both cached as ArrayBuffer at module scope in card/fonts.ts.
- Add to
DevClassunion and class scoring inpersona/classifier.ts - Add titles array (3 tiers) + emoji code (
[XX]) in classifier - Add accent hex in
getThememap (card/theme.ts) - Add to
VALID_CLASSESinroutes/card.tsso?class=override accepts it
GITHUB_TOKEN=ghp_xxxx # repo + read:user scopes
PORT=3000 # default 3000
CACHE_TTL_MS=600000 # default 10 min
CACHE_MAX_ENTRIES=500 # default 500- Project pins TypeScript
^6.0.3.tsconfig.jsonsets"ignoreDeprecations": "6.0"becausetsup --dtsinjects abaseUrlinternally that TS 6 flags as deprecated. - VS Code's bundled TS server may be older and mark
"ignoreDeprecations": "6.0"as invalid. Use "TypeScript: Select Version → Use Workspace" or ignore the IDE warning; CLIpnpm typecheckandpnpm buildboth pass.
- No test suite yet — verify visually via
pnpm devand curl - Cache is in-memory only — restart drops both caches; horizontal scaling would need a shared store