Skip to content

Latest commit

 

History

History
184 lines (130 loc) · 10.2 KB

File metadata and controls

184 lines (130 loc) · 10.2 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

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>

Tech Stack

  • Runtime: Node.js + TypeScript (ESM, .js import 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

Commands

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.

Architecture

Request flow:

HTTP → routes/card.ts
  → cardCache.getOrSet(key)
    → fetchGitHubData (cached) → classifyPersona → renderCard
      → buildBoardingPass (Satori object tree) → satori() → SVG post-processing → optional resvg PNG

Module map (src/)

  • 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+LRU createCache<T>({ttlMs, max}). getOrSet dedups concurrent calls via in-flight Promise map. LRU touch on get (Map insertion-order = recency)
  • github/fetcher.ts — GraphQL fetch + calculateCurrentStreak. fetchGitHubData is cached wrapper; fetchGitHubDataRaw does the network call
  • persona/classifier.ts — class detection (language scoring + DevOps weight boost + Fullstack/Bard rules), XP, level, stats, title
  • card/theme.tsgetTheme(class, mode), getRarity(level), layout constants (CARD_WIDTH/HEIGHT, MAIN_WIDTH, STUB_WIDTH, SPACER_WIDTH)
  • card/primitives.tsdivider, label, val (Satori-tree helpers)
  • card/fonts.ts — TTF loaders, cached at module scope
  • card/icons.ts — inline SVG icons + svgToDataUrl() for <img src> in Satori
  • card/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 / footer
  • card/sections/stub.ts — right section: gate, level, class, rarity, XP, stars, commits, PRs, issued date, streak
  • card/renderer.ts — orchestrator: buildBoardingPass composition + 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)

Caching layers

Two separate caches share the same TTL/max config:

  1. GitHub user data (github/fetcher.ts) — key = username.toLowerCase(). Shared by /api/card and /api/persona.
  2. 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.

Card layout (720 × 300)

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     │
                                                       └────────────┘

Brand randomization

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.

Booking ref

{USERNAME[0..3].toUpperCase().padEnd(3,'X')}-{XP_LAST4}-{CLASS_INITIALS}. Example: JST-3847-OB.

Streak calculation

calculateCurrentStreak in github/fetcher.ts: flatten contributionCalendar.weeks descending, skip today if 0 (day not over), count consecutive non-zero days backward.

Scoring formulas (persona/classifier.ts)

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.

Output scaling

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.

Render pipeline order (important)

renderCard in card/renderer.ts applies post-Satori transforms in this exact order:

  1. injectHeaderStripes (SVG only) — appends stripes <image> before </svg>
  2. postProcess (vignette overlay etc.) — adds <rect> overlay before </svg>
  3. 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.

Satori constraints (must follow)

  • Every div with multiple children must have display: flex
  • No position: absolute on children without explicit parent positioning
  • No grid, no clip-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
  • transform not 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 vs SVG differences

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.

Fonts

Static TTF in assets/fonts/:

  • Rajdhani-Bold.ttf — headers, labels, class names, stats
  • Inter-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.

Adding a new developer class

  1. Add to DevClass union and class scoring in persona/classifier.ts
  2. Add titles array (3 tiers) + emoji code ([XX]) in classifier
  3. Add accent hex in getTheme map (card/theme.ts)
  4. Add to VALID_CLASSES in routes/card.ts so ?class= override accepts it

Environment

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

Build quirks

  • Project pins TypeScript ^6.0.3. tsconfig.json sets "ignoreDeprecations": "6.0" because tsup --dts injects a baseUrl internally 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; CLI pnpm typecheck and pnpm build both pass.

Notes

  • No test suite yet — verify visually via pnpm dev and curl
  • Cache is in-memory only — restart drops both caches; horizontal scaling would need a shared store