feat(web): use static SVG asset for album fallback cover

Replaces the hand-rolled inline-data-URL placeholder with a static SVG
asset at /placeholders/album-fallback.svg. SvelteKit's adapter-static
publishes everything in web/static/ at the URL root and the Go binary
embeds the build output, so this resolves identically in dev and prod.

Operator can swap the artwork by replacing the file — no code changes.

Test updated to use toContain rather than strict equality, since jsdom
resolves relative URLs against the test origin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 23:21:25 -04:00
parent e6552f4ae2
commit 564e0bf7ca
3 changed files with 20 additions and 25 deletions
+4 -2
View File
@@ -62,11 +62,13 @@ describe('AlbumCard', () => {
expect(screen.queryByText(/1959/)).not.toBeInTheDocument();
});
test('broken cover falls back to FALLBACK_COVER data URL', async () => {
test('broken cover falls back to FALLBACK_COVER asset', async () => {
const { container } = render(AlbumCard, { props: { album } });
const img = container.querySelector('img') as HTMLImageElement;
await fireEvent.error(img);
expect(img.src).toBe(FALLBACK_COVER);
// jsdom resolves relative URLs against the test origin; assert containment
// rather than strict equality to keep the test agnostic of the host.
expect(img.src).toContain(FALLBACK_COVER);
});
test('+ queue button fetches album detail and calls enqueueTracks', async () => {
+7 -23
View File
@@ -1,26 +1,10 @@
// Fallback cover — an inline SVG data URL showing a beamed-eighth-notes
// glyph on a slate field. Used by AlbumCard / CompactTrackCard / PlayerBar's
// onerror handler when the real cover fails (e.g. scanner didn't pick up
// embedded art and there's no sidecar). Data URL avoids an extra HTTP
// round-trip; SVG keeps it crisp at any size we render.
//
// The glyph is two filled note heads connected by a slanted beam — bolder
// and more recognisable than a single-note silhouette, while staying muted
// enough to read as a placeholder rather than real content.
export const FALLBACK_COVER =
'data:image/svg+xml;utf8,' +
encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet">
<rect width="100" height="100" fill="#1f1f23"/>
<g fill="#7a7a82">
<ellipse cx="30" cy="72" rx="10" ry="8"/>
<rect x="38" y="34" width="4" height="40"/>
<ellipse cx="68" cy="64" rx="10" ry="8"/>
<rect x="76" y="26" width="4" height="40"/>
<polygon points="38,34 80,26 80,34 38,42"/>
</g>
</svg>`
);
// Fallback cover used when the real cover fails to load (e.g. scanner
// didn't pick up embedded art and there's no sidecar). Served as a static
// asset from web/static/placeholders/ so the operator can swap the artwork
// without touching code. SvelteKit + adapter-static publishes everything
// under web/static/ at the URL root, and the Go binary embeds the build
// output, so this resolves identically in dev and production.
export const FALLBACK_COVER = '/placeholders/album-fallback.svg';
export function coverUrl(albumId: string): string {
return `/api/albums/${albumId}/cover`;
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="windows-1252"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" xml:space="preserve">
<g>
<g>
<path d="M503.319,5.939c-5.506-4.705-12.783-6.767-19.958-5.635L169.555,49.852c-12.04,1.901-20.909,12.28-20.909,24.47v99.097 v156.903H99.097C44.455,330.323,0,371.073,0,421.161C0,471.25,44.455,512,99.097,512c54.642,0,99.097-40.75,99.097-90.839v-66.065 V194.588l264.258-41.725v136.169h-49.548c-54.642,0-99.097,40.75-99.097,90.839s44.455,90.839,99.097,90.839 S512,429.959,512,379.871v-66.065V123.871V24.774C512,17.529,508.827,10.646,503.319,5.939z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 809 B