Skip to content

Duplicate SVG clipPath IDs when sponsors share the same avatar #121

Description

@lgc2333

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 }],
})
Image

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

  • Follow our Code of Conduct
  • Read the Contributing Guide.
  • Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
  • Check that this is a concrete bug. For Q&A, please open a GitHub Discussion instead.
  • The provided reproduction is a minimal reproducible of the bug.

Contributions

  • I am willing to submit a PR to fix this issue
  • I am willing to submit a PR with failing tests (actually just go ahead and do it, thanks!)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions