diff --git a/web/src/lib/components/AlbumCard.svelte b/web/src/lib/components/AlbumCard.svelte
new file mode 100644
index 00000000..4ce23b53
--- /dev/null
+++ b/web/src/lib/components/AlbumCard.svelte
@@ -0,0 +1,27 @@
+
+
+
+
+ {album.title}
+ {#if album.year}
+ {album.year}
+ {/if}
+
diff --git a/web/src/lib/components/AlbumCard.test.ts b/web/src/lib/components/AlbumCard.test.ts
new file mode 100644
index 00000000..5eb6d4aa
--- /dev/null
+++ b/web/src/lib/components/AlbumCard.test.ts
@@ -0,0 +1,42 @@
+import { describe, expect, test } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/svelte';
+import AlbumCard from './AlbumCard.svelte';
+import type { AlbumRef } from '$lib/api/types';
+import { FALLBACK_COVER } from '$lib/media/covers';
+
+const album: AlbumRef = {
+ id: 'xyz',
+ title: 'Kind of Blue',
+ artist_id: 'm-davis',
+ artist_name: 'Miles Davis',
+ year: 1959,
+ track_count: 5,
+ duration_sec: 2630,
+ cover_url: '/api/albums/xyz/cover',
+};
+
+describe('AlbumCard', () => {
+ test('renders cover, title, year inside a link to /albums/:id', () => {
+ const { container } = render(AlbumCard, { props: { album } });
+ const link = screen.getByRole('link');
+ expect(link).toHaveAttribute('href', '/albums/xyz');
+ expect(link).toHaveTextContent('Kind of Blue');
+ expect(link).toHaveTextContent('1959');
+
+ const img = container.querySelector('img') as HTMLImageElement;
+ expect(img.src).toContain('/api/albums/xyz/cover');
+ });
+
+ test('year is omitted when not present', () => {
+ render(AlbumCard, { props: { album: { ...album, year: undefined } } });
+ // No crash; no year text.
+ expect(screen.queryByText(/1959/)).not.toBeInTheDocument();
+ });
+
+ test('broken cover falls back to FALLBACK_COVER data URL', async () => {
+ const { container } = render(AlbumCard, { props: { album } });
+ const img = container.querySelector('img') as HTMLImageElement;
+ await fireEvent.error(img);
+ expect(img.src).toBe(FALLBACK_COVER);
+ });
+});