Skip to content

Nested compositions that reuse SVG ids bind url(#…) / <use href="#"> to the first scene after inline #3490

Description

@valeriangalliat

Describe the bug

Two nested scenes that each define their own #clip, #shape, and #fx are legal per file and pass hyperframes check. After inline, the assembled document has duplicate IDs. Browser-native references (clip-path="url(#clip)", <use href="#shape">, CSS filter: url(#fx)) resolve to the first match in document order, so later scenes clip to nothing or paint the earlier scene's colour.

getElementById was scoped in #646. Media pipeline collisions were fixed with render keys in #3340. Native url(#…) / href="#…" were not rewritten, and lint does not flag generic duplicate ids — only (some) media ones.

Catalog blocks already hardcode ids such as url(#tracing-beam-glow), so two instances of the same block hit this without the author duplicating anything by hand.

Ask: rewrite url(#) / href="#" (and the matching ids) per composition on inline, or lint generic duplicate ids the way media ids are now covered. Rewrite is the one that makes nested catalog blocks work without unique-prefixing every SVG id.

Link to reproduction

https://github.com/ArcadeHQ/hyperframes-repros/tree/patch/duplicate-svg-ids

Steps to reproduce

  1. Clone https://github.com/ArcadeHQ/hyperframes-repros and check out patch/duplicate-svg-ids.
  2. Run npx hyperframes@latest check — 0 errors.
  3. Run npx hyperframes@latest snapshot --at 1,3 --no-end --describe false
  4. Open snapshots/frame-00-at-1s.png and snapshots/frame-01-at-3s.png.

Expected behavior

  • t=1s (scene-a): all four rows red
  • t=3s (scene-b): all four rows blue

Actual behavior

  • t=1s: all four rows red (ok)
  • t=3s: (1) control blue; (2) clip-path absent; (3) <use> red (scene-a's shape); (4) CSS filter red (scene-a's flood)

Environment

hyperframes doctor

  ✓ Version          0.8.14 (latest)
  ✓ Node.js          v24.19.0 (darwin arm64)
  ✓ CPU              10 cores · Apple M1 Pro @ 2400MHz
  ✓ Memory           16.0 GB total · 4.2 GB available
  ✓ Disk             365.8 GB free
  ✓ Frames cache     /var/folders/bl/c4hk6r7n34g1d81txd99gsgr0000gn/T/hyperframes-extract-cache-501 · 365.8 GB free at /var/folders/bl/c4hk6r7n34g1d81txd99gsgr0000gn/T/hyperframes-extract-cache-501 · default
  ✓ Archive extractor unzip
  ✓ Environment      non-TTY
  ✗ whisper-cpp      Not found (optional — needed for transcription)
  ✗ TTS (Kokoro)     Not installed (optional — local voice fallback)
  ✗ BGM (MusicGen)   Not installed (optional — local music fallback)
  ✓ FFmpeg           ffmpeg 8.1.2 at /opt/homebrew/bin/ffmpeg
  ✓ FFprobe          ffprobe 8.1.2 at /opt/homebrew/bin/ffprobe
  ✓ Chrome           cache: /Users/val/.cache/puppeteer/chrome-headless-shell/mac_arm-152.0.7977.42/chrome-headless-shell-mac-arm64/chrome-headless-shell
  ✗ Docker           Not found
  ✗ Docker running   Not running

Additional context

Related, not the same: #646 (scoped getElementById), #3340 (media render keys). #556 was CSS attribute-selector scope across sibling instances.

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