Skip to content

feat: improve badge width estimation - #2487

Merged
ghostdevv merged 3 commits into
npmx-dev:mainfrom
t128n:fix/text-measurement-without-canvas-api
May 11, 2026
Merged

feat: improve badge width estimation#2487
ghostdevv merged 3 commits into
npmx-dev:mainfrom
t128n:fix/text-measurement-without-canvas-api

Conversation

@t128n

@t128nt128n commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

🔗 Linked issue

Resolves#1601

🧭 Context

In production, measuring text with canvas is crashing, resulting in inaccurate fallback measurements:

overflowing badge

Introduced a character lookup table of "manually" measured character widths for more
accurate lookups.

📚 Description

Tweaked the estimateTextWidth function to use a on-character level lookup table for more accurate results when the Canvas API from @napi-rs/canvas is unavailable.
Lookup table derived from running

// ...constctx=createCanvas(1,1).getContext('2d')constchars=' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'ctx.font=BADGE_FONT_SHORTHANDconstentries=[...chars].map(ch=>`'${ch==="'" ? "\\'" : ch==='\\' ? '\\\\' : ch}': ${Math.ceil(ctx.measureText(ch).width)}`)console.log('default: {\n '+entries.join(', ')+'\n}')

locally, where fonts and the API are available.

Using a generous fallback for characters that aren't in the map, such as emojis.

📷 Comparison

Overflow / Long Labels

CaseProdPR
Long label
Long value
Long package name

Shields.io Style (?style=shieldsio)

CaseProdPR
Normal
Long label
Emoji label

Emojis

CaseProdPR
Emoji in label
Emoji in value
Multiple emojis
Emoji-only label

CJK Characters

CaseProdPR
Chinese label
Japanese label
Korean label
Mixed CJK + ASCII

Special & Punctuation Characters

CaseProdPR
Symbols
Narrow chars (iiiii)
Wide chars (WWWWW)
Mixed widths

Badge Types Smoke Test

TypeProdPR
version
license
downloads
size
types
deprecated

@vercel

vercelBot commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
docs.npmx.devReadyReadyPreview, CommentApr 19, 2026 10:30pm
npmx.devReadyReadyPreview, CommentApr 19, 2026 10:30pm
1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
npmx-lunariaIgnoredIgnoredApr 19, 2026 10:30pm

Request Review

@coderabbitai

coderabbitaiBot commented Apr 12, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Replaced heuristic, bucket-based text width estimation with explicit per-character width lookup tables for supported badge fonts and a fixed fallback width for unmapped characters; updated estimateTextWidth to use font and sum per-character widths.

Changes

Cohort / File(s)Summary
Badge Text-Width Measurement
server/api/registry/badge/[type]/[...pkg].get.ts
Removed regex/set-based categorisation and FALLBACK_WIDTHS/NARROW_CHARS/MEDIUM_CHARS buckets. Added CHAR_WIDTHS for default and shieldsio fonts, CHAR_WIDTH_FALLBACK for unknown characters, and refactored estimateTextWidth(font, text) to sum per-character widths with fallback.

Possibly related PRs

Suggested reviewers

  • danielroe
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description check✅ PassedThe description is comprehensive and clearly related to the changeset, detailing the problem (Canvas measurement failures on Vercel), the solution (per-character lookup table), and providing extensive visual comparisons demonstrating the fix.
Linked Issues check✅ PassedThe PR successfully addresses issue #1601 by implementing a per-character lookup table for accurate text-width measurement when Canvas-based measurement fails on Vercel, preventing badge overflow and ensuring consistent rendering across environments.
Out of Scope Changes check✅ PassedAll changes are directly scoped to fixing text-width estimation for badges by replacing the heuristic approach with a lookup table; no unrelated modifications or refactoring are present.
Title check✅ PassedThe title 'feat: improve badge width estimation' is directly related to the main change: replacing a heuristic text width estimator with a deterministic per-character lookup table to improve badge width estimation accuracy.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecovBot commented Apr 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@t128nt128n changed the title fix(badges): use lookup table for char widths when canvas not availablefix: use lookup table for badges char widths when canvas not availableApr 12, 2026
@t128n
t128n marked this pull request as ready for review April 12, 2026 12:15
@serhalpserhalp added needs review This PR is waiting for a review from a maintainer ux Related to wider UX decisions labels Apr 12, 2026
@ghostdevv
ghostdevv self-requested a review April 13, 2026 18:31

@ghostdevvghostdevv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't break on the last test case from #2199 so we can yolo this :p

@ghostdevvghostdevv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, rq why is it "when canvas not available"? @t128n

@t128n

Copy link
Copy Markdown
ContributorAuthor

actually, rq why is it "when canvas not available"? @t128n

Text is usually measured using createCanvasContext. On Vercel, createCanvasContext can’t be created (returns null, likely due to missing native node.js addons in the serveless runtime), so it falls back to estimateTextWidth. estimateTextWidth makes a best guess for characters’ widths and has edge cases with wider characters like w.
That's the whole context.

So Canvas not available was referring to the createCanvasContext (or what its name is - currently on mobile and it's hard to look outside the PRs boundary to find the imports).

@ghostdevv

ghostdevv commented Apr 14, 2026

Copy link
Copy Markdown
Member

actually, rq why is it "when canvas not available"? @t128n

Text is usually measured using createCanvasContext. On Vercel, createCanvasContext can’t be created (returns null, likely due to missing native node.js addons in the serveless runtime), so it falls back to estimateTextWidth. estimateTextWidth makes a best guess for characters’ widths and has edge cases with wider characters like w. That's the whole context.

So Canvas not available was referring to the createCanvasContext (or what its name is - currently on mobile and it's hard to look outside the PRs boundary to find the imports).

ah but that's always been true right? like that fact hasn't changed in this PR?

also that's why we have the napi-rs canvas? or no?

@t128n

Copy link
Copy Markdown
ContributorAuthor

Yeah, my assumptions were flawed - I thought it would be about the whole canvas not being available, but I made some further tests real quick.

So, @napi-rs/canvas & canvasContext generally are available, but if I'm not missing anything major, it's producing 0 widths because Geist as a font is not available on the Vercel serverless function (my assumption based on the testing).

functionmeasureTextWidth(text: string,font: string): number|null{constcontext=getCanvasContext()if(context){context.font=fontconstmeasuredWidth=context.measureText(text).widthif(Number.isFinite(measuredWidth)&&measuredWidth>0){// Returns false as font not available and nothing can be measuredreturnMath.ceil(measuredWidth)}}returnnull// yields null for canvas context and then falls back to estimateTextWidth }

So it's rather: use pre-measured widths for a lookup table rather than use table for badges when canvas not available

@wojtekmaj

Copy link
Copy Markdown
Contributor

ah but that's always been true right? like that fact hasn't changed in this PR?

Yea, it is my flawed implementation that I wasn't able to debug because I didn't know what errors were happening on Vercel, and it is not reproducible locally at all.

@ghostdevv

Copy link
Copy Markdown
Member

I think we can create an issue to try and load the font and see what the perf is like on that, if we can get it working that is. If there are Vercel issues then we can help with debugging that this time around as we should now have access to logs

@ghostdevvghostdevv changed the title fix: use lookup table for badges char widths when canvas not availablefix: improve badge width estimationApr 19, 2026
@ghostdevvghostdevv changed the title fix: improve badge width estimationfeat: improve badge width estimationApr 19, 2026
@ghostdevv
ghostdevv enabled auto-merge April 19, 2026 22:34
@ghostdevvghostdevv removed the needs review This PR is waiting for a review from a maintainer label Apr 19, 2026
@serhalp

Copy link
Copy Markdown
Member

@ghostdevv what's the status of this PR? I see you enabled auto-merge but you still have changes requested 🤔🤔🤔

@t128n

t128n commented May 9, 2026

Copy link
Copy Markdown
ContributorAuthor

@ghostdevv what's the status of this PR? I see you enabled auto-merge but you still have changes requested 🤔🤔🤔

There was #2587 created to fix the actual bug (Geist font not available, and therefore the endpoint falling back to the measuring algorithm) instead of working around it by improving the text-measurement algorithm

@ghostdevvghostdevv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, rq why is it "when canvas not available"? @t128n sorry this is what was in the box when I went to approve - GitHub fully functioning software! xD I think this should be good and we can do the follow up later...

@ghostdevv
ghostdevv added this pull request to the merge queueMay 11, 2026
Merged via the queue into npmx-dev:main with commit 52b86d6May 11, 2026
23 checks passed
@github-actionsgithub-actionsBot mentioned this pull request May 11, 2026
ayo-run pushed a commit to ayo-run/npmx.dev that referenced this pull request Aug 5, 2026
Co-authored-by: Willow (GHOST) <git@willow.sh>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

uxRelated to wider UX decisions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Measuring text for the purpose of rendering badges fails on Vercel

4 participants

@t128n@ghostdevv@wojtekmaj@serhalp