diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte
index b4d4e63b..83417fa8 100644
--- a/web/src/routes/+page.svelte
+++ b/web/src/routes/+page.svelte
@@ -1,2 +1,83 @@
-
Library views land in a subsequent plan.
+
+
+
+
+
+ {#if query.isError}
+
+ {:else if showSkeleton.value && artists.length === 0}
+
+ {:else if !query.isPending && total === 0}
+
+ No artists yet — scan a library folder via the server's config.
+
+ {:else}
+
+ {#each artists as a (a.id)}
+
+ {/each}
+
+
+ {#if query.hasNextPage}
+
+ {:else if artists.length > 0}
+
End of library
+ {/if}
+ {/if}
+
diff --git a/web/src/routes/artists.test.ts b/web/src/routes/artists.test.ts
new file mode 100644
index 00000000..cc005579
--- /dev/null
+++ b/web/src/routes/artists.test.ts
@@ -0,0 +1,124 @@
+import { afterEach, describe, expect, test, vi } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/svelte';
+import { flushSync } from 'svelte';
+import { mockInfiniteQuery } from '../test-utils/query';
+import type { ArtistRef, Page } from '$lib/api/types';
+
+const state = vi.hoisted(() => ({ pageUrl: new URL('http://localhost/') }));
+
+vi.mock('$app/state', () => ({
+ page: { get url() { return state.pageUrl; } }
+}));
+
+vi.mock('$app/navigation', () => ({
+ goto: vi.fn()
+}));
+
+vi.mock('$lib/api/queries', () => ({
+ qk: { artists: (sort: string) => ['artists', { sort }] },
+ createArtistsQuery: vi.fn()
+}));
+
+import ArtistsPage from './+page.svelte';
+import { createArtistsQuery } from '$lib/api/queries';
+import { goto } from '$app/navigation';
+
+function page