From 7190bafd7028d514479af0116ebb81d86ef9e231 Mon Sep 17 00:00:00 2001 From: OM MISHRA <152969928+howwohmm@users.noreply.github.com> Date: Sun, 22 Mar 2026 18:05:22 +0530 Subject: [PATCH] fix(badge): register Geist font with canvas to fix width measurement The Geist font was not registered with @napi-rs/canvas, causing text measurement to use a wider system fallback font in production. This produced badges with excessive whitespace, especially for longer text. Register the Geist font from server assets so canvas measures text with the same font the SVG specifies for rendering. Closes #2187 Co-Authored-By: Claude Opus 4.6 --- nuxt.config.ts | 6 +++++ .../api/registry/badge/[type]/[...pkg].get.ts | 23 ++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/nuxt.config.ts b/nuxt.config.ts index 88d3822376..12c4b86585 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -253,6 +253,12 @@ export default defineNuxtConfig({ replace: { 'import.meta.test': isTest, }, + serverAssets: [ + { + baseName: 'fonts', + dir: './public/fonts', + }, + ], }, fonts: { diff --git a/server/api/registry/badge/[type]/[...pkg].get.ts b/server/api/registry/badge/[type]/[...pkg].get.ts index 2ce404e381..93d69dca70 100644 --- a/server/api/registry/badge/[type]/[...pkg].get.ts +++ b/server/api/registry/badge/[type]/[...pkg].get.ts @@ -1,5 +1,5 @@ import * as v from 'valibot' -import { createCanvas, type SKRSContext2D } from '@napi-rs/canvas' +import { createCanvas, GlobalFonts, type SKRSContext2D } from '@napi-rs/canvas' import { hash } from 'ohash' import { createError, getRouterParam, getQuery, setHeader } from 'h3' import { PackageRouteParamsSchema } from '#shared/schemas/package' @@ -52,6 +52,25 @@ const BADGE_FONT_SHORTHAND = 'normal normal 400 11px Geist, system-ui, -apple-sy const SHIELDS_FONT_SHORTHAND = 'normal normal 400 11px Verdana, Geneva, DejaVu Sans, sans-serif' let cachedCanvasContext: SKRSContext2D | null | undefined +let fontRegistered = false + +async function registerGeistFont(): Promise { + if (fontRegistered) return + + try { + const fontStorage = useStorage('assets:fonts') + const fontData = await fontStorage.getItemRaw('Geist-Regular.ttf') + + if (fontData) { + const buffer = fontData instanceof Buffer ? fontData : Buffer.from(fontData as ArrayBuffer) + GlobalFonts.register(buffer, 'Geist') + } + } catch { + // font registration is best-effort; fallback measurement will be used + } + + fontRegistered = true +} function getCanvasContext(): SKRSContext2D | null { if (cachedCanvasContext !== undefined) { @@ -431,6 +450,8 @@ const BadgeStyleSchema = v.picklist(['default', 'shieldsio']) export default defineCachedEventHandler( async event => { + await registerGeistFont() + const query = getQuery(event) const typeParam = getRouterParam(event, 'type') const pkgParamSegments = getRouterParam(event, 'pkg')?.split('/') ?? []