Skip to content

feat(card): fit displayName by length across live card + Satori render#75

Open
rgarciar1931 wants to merge 1 commit into
Younesfdj:masterfrom
rgarciar1931:feat/fit-card-names
Open

feat(card): fit displayName by length across live card + Satori render#75
rgarciar1931 wants to merge 1 commit into
Younesfdj:masterfrom
rgarciar1931:feat/fit-card-names

Conversation

@rgarciar1931

@rgarciar1931 rgarciar1931 commented Jul 15, 2026

Copy link
Copy Markdown

Summary

Add a length-aware font-size step so long GitHub display names stay inside the FUT card's visible name band, and keep the Satori OG render visually identical to the live card.

Problem

Long GitHub display names — unbroken uppercase tokens like VERY-LONG-USERNAME, all-caps surnames with no separator, or single-token logins >16 chars — overflowed the FUT card's centered name slot at the previous fixed 13cqw font-size. They bled past the under-name separator (H_LINES[2]) on either side and clipped off the card's edge.

What changed

  • lib/text.ts — added getFitFontSize(displayName: string): number, a pure step function keyed on the already-uppercased display-name length:
    • ≤10 chars → 13 cqw
    • ≤13 → 11.5 cqw
    • ≤16 → 10 cqw
    • ≤19 → 9 cqw
    • ≤22 → 8.2 cqw
    • >22 → 7.5 cqw
      Co-located next to cardDisplayName so the helper is shared between the React tree and Satori.
  • components/PlayerCard.tsx — name wrapper swapped from left: 50% + translateX(-50%) (single text node) to left: 0; width: 100%; text-align: center (block wrapper containing an inner display: inline-block; white-space: nowrap span that reads the helper). Visual position is identical; the text now scales with length bracket.
  • lib/og/renderCard.tsx — same cqw(getFitFontSize(displayName)) inside the existing flex-centered outer div so the Satori OG image matches the live card byte-for-byte without DOM measurement on the server.
  • tests/text.test.ts (new) — boundary cases (≤10 / ≤13 / ≤16 / ≤19 / ≤22 / >22) plus a monotonicity check, and a regression for cardDisplayName.

Why a step function (not ResizeObserver + scrollWidth measurement)

Two reasons collide:

  1. Satori has no DOM. The OG image and the html-to-image export both render without measuring — they need pure input. A pure length-aware function takes the same displayName and produces the same font-size on every render path.
  2. No hydration flash. A useLayoutEffect measure-and-fit would change the rendered font-size between SSR and CSR (the SSR HTML would have the un-measured size until the effect runs), triggering React's hydration mismatch warning on the card's displayName text node. The step function gives identical output on both sides.

Visual comparison

  • Before (long display name at 13cqw): bleeds past the under-name separator on either side, clipping the edge of the card.
  • After (same name, fits the right length bracket): sits inside the visible band cleanly across viewports.

Validation

  • npm run lint — clean (12 warnings, 0 errors — same baseline as master).
  • npm test — all tests pass on feat/fit-card-names tip d16a548.

Example

  • Before:
image
  • After:
image

Long GitHub display names (e.g. unbroken uppercase tokens like
"ABCDEFGHIJKLMNOPQRS", 19+ chars) overflowed the FUT card's visible band
at the previous fixed 13cqw font-size. Add a pure length-aware
getFitFontSize(displayName) step function in lib/text.ts (cqw:
13 → 11.5 → 10 → 9 → 8.2 → 7.5, keyed on the already-uppercased
display-name length). Replace the PlayerCard name wrapper's left:50%
+ translateX(-50%) layout with a width:100% + text-align:center outer
div carrying an inner display:inline-block span that reads the helper,
and consume the same helper from the Satori OG render in
lib/og/renderCard.tsx so the export and OG image are visually identical
to the screen without DOM measurement on the server.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant