Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision 732272a320793693be207b1b67af729b1feca30f)
+++ src/routes/activitypub.js	(revision 5e16b8aa5bff59005d55b10e60ba39fb7989e733)
@@ -1115,5 +1115,5 @@
      ORDER BY pinned DESC, COALESCE(published_at, created_at) ASC LIMIT 20`
   ).all(site.id);
-  AP.sendAP(res, AP.buildFeatured(baseUrl(req), site, posts));
+  AP.sendAP(res, AP.buildFeatured(baseUrl(req), site, posts, { page: paginaNr(req) }));
 });
 
@@ -1130,5 +1130,5 @@
   const site = publicSite(req.params.slug);
   if (!site) return res.status(404).end();
-  AP.sendAP(res, AP.listPlaylistsAP(baseUrl(req), site, wantsEnriched(req, res)));
+  AP.sendAP(res, AP.listPlaylistsAP(baseUrl(req), site, wantsEnriched(req, res), { page: paginaNr(req) }));
 });
 
@@ -1139,5 +1139,5 @@
   const site = publicSite(req.params.slug);
   if (!site) return res.status(404).end();
-  AP.sendAP(res, AP.buildTrackCollection(baseUrl(req), site, AP.siteOpenTracks(site.id, { alles: await magAlles(req, site.slug) })));
+  AP.sendAP(res, AP.buildTrackCollection(baseUrl(req), site, AP.siteOpenTracks(site.id, { alles: await magAlles(req, site.slug) }), { page: paginaNr(req) }));
 });
 
@@ -1150,8 +1150,14 @@
 // Openbaar, want alles erin is fedi_open. Er valt dus niets goed te keuren en de
 // volgerslijst blijft leeg: wie ons volgt volgt de ACTOR, niet de bak.
+//
+// `?page=` MOET hier doorgegeven worden. Zonder dat adverteert de wortel een
+// `first` die op zichzelf uitkomt: de lezer volgt hem, krijgt weer een `Library`
+// in plaats van een pagina, en klapt eruit -- open.audio gaf op 15-8 een 500 op
+// precies deze URL. Dezelfde les als bij de outbox (shaer-sk4): een `first`
+// beloven is een pagina beloven.
 router.get('/ap/users/:slug/library', (req, res) => {
   const site = publicSite(req.params.slug);
   if (!site) return res.status(404).end();
-  AP.sendAP(res, AP.buildLibrary(baseUrl(req), site, AP.siteOpenTracks(site.id)));
+  AP.sendAP(res, AP.buildLibrary(baseUrl(req), site, AP.siteOpenTracks(site.id), { page: paginaNr(req) }));
 });
 
@@ -1161,5 +1167,5 @@
   const site = publicSite(req.params.slug);
   if (!site) return res.status(404).end();
-  AP.sendAP(res, AP.pagedCollection(`${AP.libraryId(baseUrl(req), site)}/followers`, []));
+  AP.sendAP(res, AP.pagedCollection(`${AP.libraryId(baseUrl(req), site)}/followers`, [], { page: paginaNr(req) }));
 });
 
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 732272a320793693be207b1b67af729b1feca30f)
+++ src/services/ActivityPubService.js	(revision 5e16b8aa5bff59005d55b10e60ba39fb7989e733)
@@ -1200,8 +1200,8 @@
 // these as the "Featured" tab (pinned to the profile). Posts come ordered by pin
 // rank; embedded as full Notes so a remote server doesn't need extra fetches.
-export function buildFeatured(base, site, posts) {
+export function buildFeatured(base, site, posts, { page = false } = {}) {
   const id = `${actorId(base, site.slug)}/featured`;
   const items = (posts || []).map((p) => buildNote(base, site, p));
-  return pagedCollection(id, items);
+  return pagedCollection(id, items, { page });
 }
 
Index: src/services/music/index.js
===================================================================
--- src/services/music/index.js	(revision 732272a320793693be207b1b67af729b1feca30f)
+++ src/services/music/index.js	(revision 5e16b8aa5bff59005d55b10e60ba39fb7989e733)
@@ -247,9 +247,10 @@
  * ooit een besloten variant, dan hoort daar het Follow/Accept-werk bij.
  */
-export function buildLibrary(base, site, rows) {
+export function buildLibrary(base, site, rows, { page = false } = {}) {
   const id = libraryId(base, site);
   const hostPosts = site.id ? trackHostPosts(site.id) : null;
   const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { hostPosts }));
   return pagedCollection(id, items, {
+    page,
     extra: {
       type: 'Library',
@@ -265,9 +266,9 @@
 
 /** De collectie van alle open tracks van een site (shaer-0nh, stap 3). */
-export function buildTrackCollection(base, site, rows) {
+export function buildTrackCollection(base, site, rows, { page = false } = {}) {
   // Eén zoekopdracht voor alle rijen samen; zie trackHostPosts.
   const posts = site.id ? trackHostPosts(site.id) : null;
   const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { hostPosts: posts }));
-  return pagedCollection(`${actorId(base, site.slug)}/tracks`, items, { extra: { attributedTo: actorId(base, site.slug) } });
+  return pagedCollection(`${actorId(base, site.slug)}/tracks`, items, { page, extra: { attributedTo: actorId(base, site.slug) } });
 }
 
@@ -314,5 +315,5 @@
 // collectie zelf, want ook een lijst mag niet verklappen wat er achter de
 // poort staat.
-export function listPlaylistsAP(base, site, enriched) {
+export function listPlaylistsAP(base, site, enriched, { page = false } = {}) {
   const rows = db.prepare(
     'SELECT id, title, artist, year, cover_url FROM playlists WHERE site_id = ? ORDER BY created_at, id'
@@ -327,5 +328,5 @@
     return stub;
   });
-  return pagedCollection(colId, items, { extra: { attributedTo: actorId(base, site.slug) } });
+  return pagedCollection(colId, items, { page, extra: { attributedTo: actorId(base, site.slug) } });
 }
 
Index: test/ap-collection-paging.test.js
===================================================================
--- test/ap-collection-paging.test.js	(revision 5e16b8aa5bff59005d55b10e60ba39fb7989e733)
+++ test/ap-collection-paging.test.js	(revision 5e16b8aa5bff59005d55b10e60ba39fb7989e733)
@@ -0,0 +1,92 @@
+// Wie `first` belooft, moet een PAGINA leveren (open.audio, 15-8).
+//
+// De aanleiding: `/ap/users/dev/library` gaf 200, keurig met `first` en `last`,
+// maar `?page=1` gaf de bibliotheek OPNIEUW -- zelfde id, type Library, en een
+// `first` die weer naar zichzelf wees. open.audio volgde die verwijzing en gaf
+// een 500 terug op onze URL.
+//
+// De bouwer kon al pagineren; de ROUTE gaf `?page=` niet door. Daarom loopt deze
+// test over HTTP en niet over `buildLibrary()`: een unit-test op de bouwer stond
+// er al en was groen, precies terwijl het gat openstond. Zie
+// [verificatie-niet-zonder-slaagtest] -- een controle die niet kan falen op wat
+// je verandert is geen bewijs.
+//
+// Hij is bewust een LUS over alle collecties en geen rijtje losse gevallen: het
+// gat ontstond doordat vier routes de doorgifte wel hadden en twee niet, en een
+// per-collectie-test vraagt om precies diezelfde vergeetachtigheid.
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+
+process.env.DATABASE_PATH = ':memory:';
+process.env.PUBLIC_BASE_URL = 'https://test.example';
+
+const dbMod = await import('../src/config/database.js');
+const db = dbMod.default;
+dbMod.initializeDatabase();
+const express = (await import('express')).default;
+const routes = (await import('../src/routes/activitypub.js')).default;
+
+db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
+  .run('u1', 'u1', 'u1@t', 'x', 'god');
+db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_public, is_primary) VALUES (?,?,?,?,1,1)')
+  .run('s1', 'band', 'De Band', 'u1');
+
+const insM = db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,1)');
+const insT = db.prepare('INSERT INTO audio_tracks (id, site_id, title, artist, duration, media_id, fedi_open, position) VALUES (?,?,?,?,?,?,1,?)');
+for (const n of [1, 2, 3]) {
+  insM.run(`m${n}`, 's1', `t${n}.mp3`, `audio/t${n}.mp3`, 'audio/mpeg');
+  insT.run(`t${n}`, 's1', `Nummer ${n}`, 'De Band', 120, `m${n}`, n);
+}
+db.prepare("INSERT INTO playlists (id, site_id, title, kind) VALUES ('plaat','s1','De Plaat','album')").run();
+db.prepare("INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES ('plaat','t1',1)").run();
+db.prepare(`INSERT INTO posts (id, site_id, author_id, slug, title, content, status, published_at)
+            VALUES ('p1','s1','u1','hallo','Hallo','<p>x</p>','published','2026-08-01T00:00:00Z')`).run();
+
+const app = express(); app.use(routes);
+const server = app.listen(0);
+await new Promise((r) => server.once('listening', r));
+const base = `http://127.0.0.1:${server.address().port}`;
+
+const haal = async (pad) => {
+  const r = await fetch(base + pad, { headers: { Accept: 'application/activity+json' } });
+  return { status: r.status, body: r.status === 200 ? await r.json() : null };
+};
+
+// Elke collectie die een actor of een object naar buiten adverteert.
+const COLLECTIES = [
+  '/ap/users/band/outbox',
+  '/ap/users/band/followers',
+  '/ap/users/band/following',
+  '/ap/users/band/featured',
+  '/ap/users/band/tracks',
+  '/ap/users/band/playlists',
+  '/ap/users/band/library',
+  '/ap/users/band/library/followers',
+];
+
+for (const pad of COLLECTIES) {
+  test(`${pad}: first wijst naar een echte pagina`, async () => {
+    const wortel = await haal(pad);
+    assert.equal(wortel.status, 200, `${pad} gaf ${wortel.status}`);
+
+    // Geen `first`? Dan belooft hij ook niets en valt er niets te controleren.
+    if (!wortel.body.first) return;
+
+    const eerste = typeof wortel.body.first === 'string' ? wortel.body.first : wortel.body.first.id;
+    assert.ok(eerste, `${pad}: first zonder id`);
+
+    const pagina = await haal(new URL(eerste).pathname + new URL(eerste).search);
+    assert.equal(pagina.status, 200, `${pad}: first gaf ${pagina.status}`);
+
+    // DE KERN: de pagina moet een pagina zijn en niet de collectie opnieuw.
+    assert.match(pagina.body.type, /Page$/, `${pad}: first gaf type ${pagina.body.type}`);
+    assert.equal(pagina.body.partOf, wortel.body.id, `${pad}: pagina hoort niet bij de collectie`);
+    assert.notEqual(pagina.body.id, wortel.body.id, `${pad}: pagina deelt zijn id met de collectie`);
+
+    // En hij mag niet zelf weer een `first` dragen -- dat is de lus waar
+    // open.audio in liep.
+    assert.equal(pagina.body.first, undefined, `${pad}: de pagina draagt zelf een first`);
+  });
+}
+
+test.after(() => server.close());
