This issue was written by an LLM.
Describe the bug
SponsorKit used to crop circular avatars during image processing with sharp, so each rendered avatar did not need an SVG-level clipping definition.
After #95, circular cropping was moved into the generated SVG by emitting a <clipPath> for each avatar. That introduced per-avatar clipPath ids based on an incrementing counter. This avoided duplicate ids, but because badges are generated asynchronously, the counter assignment order could vary between runs, which caused the non-idempotent SVG output reported in #109.
#110 fixed #109 by making the clipPath id deterministic:
const cropId = `c${crypto.createHash("md5").update(base64Image).digest("hex").slice(0, 6)}`
However, this makes the id deterministic per image content, not per rendered avatar instance. When two or more sponsors use the same image data, for example the same fallback/default avatar, SponsorKit emits multiple elements with the same id.
In SVG, duplicate ids are invalid, and browsers resolve clip-path="url(#...)" against the first matching element. As a result, later avatars that share the same image can be clipped by the first avatar's rectangle instead of their own position, making those avatars appear blank or transparent in the generated SVG.
So this is a regression from the hash-based fix for #109: the SVG output is now stable, but shared-avatar cases can render incorrectly because clipPath ids are no longer unique per crop instance.
Proposed Fix
Use the deterministic render-order index directly for the SVG-local clipPath id.
The id only needs to be unique within the generated SVG document, so it does not need to be derived from the avatar image bytes. As long as the index comes from the already-determined sponsor/layout order, repeated generation with unchanged sponsor data will still produce the same SVG.
Reproduction
// sponsorkit.config.mjs
import { createHash } from 'node:crypto'
import { defineConfig, tierPresets } from 'sponsorkit'
const gravatarHash = createHash('md5').update('lgc2333@126.com').digest('hex')
const fallbackAvatar = `https://www.gravatar.com/avatar/${gravatarHash}?s=240&d=identicon`
const provider = {
name: 'local',
async fetchSponsors() {
return [
{
sponsor: {
type: 'User',
login: 'alice',
name: 'Alice',
avatarUrl: '',
},
monthlyDollars: 20,
},
{
sponsor: {
type: 'User',
login: 'bob',
name: 'Bob',
avatarUrl: '',
},
monthlyDollars: 10,
},
]
},
}
export default defineConfig({
providers: [provider],
outputDir: './sponsorkit',
formats: ['svg'],
fallbackAvatar,
tiers: [{ preset: tierPresets.large }],
})
System Info
System:
OS: Windows 11 10.0.26200
CPU: (12) x64 Intel(R) Core(TM) i5-10400F CPU @ 2.90GHz
Memory: 16.37 GB / 31.91 GB
Binaries:
Node: 24.16.0 - D:\Program Files\nodejs\node.EXE
npm: 11.13.0 - D:\Program Files\nodejs\npm.CMD
pnpm: 11.15.1 - D:\Program Files\nodejs\pnpm.CMD
Browsers:
Chrome: 150.0.7871.129
Edge: Chromium (149.0.4022.52)
Observed with sponsorkit@17.1.1.
Used Package Manager
pnpm
Validations
Contributions
Describe the bug
SponsorKit used to crop circular avatars during image processing with
sharp, so each rendered avatar did not need an SVG-level clipping definition.After #95, circular cropping was moved into the generated SVG by emitting a
<clipPath>for each avatar. That introduced per-avatarclipPathids based on an incrementing counter. This avoided duplicate ids, but because badges are generated asynchronously, the counter assignment order could vary between runs, which caused the non-idempotent SVG output reported in #109.#110 fixed #109 by making the
clipPathid deterministic:However, this makes the id deterministic per image content, not per rendered avatar instance. When two or more sponsors use the same image data, for example the same fallback/default avatar, SponsorKit emits multiple elements with the same id.
In SVG, duplicate ids are invalid, and browsers resolve clip-path="url(#...)" against the first matching element. As a result, later avatars that share the same image can be clipped by the first avatar's rectangle instead of their own position, making those avatars appear blank or transparent in the generated SVG.
So this is a regression from the hash-based fix for #109: the SVG output is now stable, but shared-avatar cases can render incorrectly because clipPath ids are no longer unique per crop instance.
Proposed Fix
Use the deterministic render-order index directly for the SVG-local
clipPathid.The id only needs to be unique within the generated SVG document, so it does not need to be derived from the avatar image bytes. As long as the index comes from the already-determined sponsor/layout order, repeated generation with unchanged sponsor data will still produce the same SVG.
Reproduction
System Info
Used Package Manager
pnpm
Validations
Contributions