Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision cb3001e6d3bf4bb9612f2f29fd9500d493bf84bb)
+++ src/routes/activitypub.js	(revision aed0092c4492bca99e1c9a5c82332c1d08fe1bee)
@@ -1204,4 +1204,7 @@
   // zijn -- anders springt het hartje terug zodra de reader opnieuw opent.
   const reacties = AP.getReactionsFor(auth.site.slug, uit.notes.map((n) => n.id));
+  // De thread heeft al een ?object= in zijn id, dus geen ?page= erachter: die
+  // collectie is niet te pagineren zonder de vraag zelf te herhalen. Hij is
+  // owner-only en wordt door Shaer gelezen, niet door de federatie.
   AP.sendAP(res, {
     '@context': AP.AP_CONTEXT,
@@ -1221,11 +1224,5 @@
   const base = baseUrl(req);
   const items = AP.getReplyUris(base, req.params.id);
-  AP.sendAP(res, {
-    '@context': AP.AP_CONTEXT,
-    id: `${base}/ap/notes/${req.params.id}/replies`,
-    type: 'OrderedCollection',
-    totalItems: items.length,
-    orderedItems: items,
-  });
+  AP.sendAP(res, AP.pagedCollection(`${base}/ap/notes/${req.params.id}/replies`, items));
 });
 
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision cb3001e6d3bf4bb9612f2f29fd9500d493bf84bb)
+++ src/services/ActivityPubService.js	(revision aed0092c4492bca99e1c9a5c82332c1d08fe1bee)
@@ -29,5 +29,5 @@
 import Blocklist from './BlocklistService.js';
 import * as Guardianship from './guardianship/index.js';
-import { PUBLIC, AP_CONTEXT, safeUrl, actorId, noteId, guessMediaType, normalizeTags, tagParts, hashtagTags, buildHashtagList } from './ap-core.js';
+import { PUBLIC, AP_CONTEXT, safeUrl, actorId, noteId, guessMediaType, normalizeTags, tagParts, hashtagTags, buildHashtagList, pagedCollection } from './ap-core.js';
 // Doorgeven wat hier altijd vandaan kwam, zodat elke bestaande aanroep blijft werken.
 export { AP_CONTEXT, actorId, noteId, guessMediaType };
@@ -1043,24 +1043,5 @@
   // wie hem vandaag leest hoort er morgen niet voor te hoeven pagineren. Er is
   // precies een pagina, dus first en last wijzen naar dezelfde.
-  const eerste = `${id}?page=1`;
-  if (page) {
-    return {
-      '@context': AP_CONTEXT,
-      id: eerste,
-      type: 'OrderedCollectionPage',
-      partOf: id,
-      totalItems: items.length,
-      orderedItems: items,
-    };
-  }
-  return {
-    '@context': AP_CONTEXT,
-    id,
-    type: 'OrderedCollection',
-    totalItems: items.length,
-    first: eerste,
-    last: eerste,
-    orderedItems: items,
-  };
+  return pagedCollection(id, items, { page });
 }
 
@@ -1070,11 +1051,6 @@
 export function buildFollowers(base, site, count, items = null) {
   const id = `${actorId(base, site.slug)}/followers`;
-  return {
-    '@context': AP_CONTEXT,
-    id,
-    type: 'OrderedCollection',
-    totalItems: items ? items.length : (count || 0),
-    orderedItems: items || [], // count-only for the public; full for the owner
-  };
+  // count-only for the public; full for the owner
+  return pagedCollection(id, items || [], { totalItems: items ? items.length : (count || 0) });
 }
 
@@ -1083,11 +1059,6 @@
 export function buildFollowing(base, site, count, items = null) {
   const id = `${actorId(base, site.slug)}/following`;
-  return {
-    '@context': AP_CONTEXT,
-    id,
-    type: 'OrderedCollection',
-    totalItems: items ? items.length : (count || 0),
-    orderedItems: items || [], // count-only for the public; full for the owner
-  };
+  // count-only for the public; full for the owner
+  return pagedCollection(id, items || [], { totalItems: items ? items.length : (count || 0) });
 }
 
@@ -1098,11 +1069,5 @@
   const id = `${actorId(base, site.slug)}/featured`;
   const items = (posts || []).map((p) => buildNote(base, site, p));
-  return {
-    '@context': AP_CONTEXT,
-    id,
-    type: 'OrderedCollection',
-    totalItems: items.length,
-    orderedItems: items,
-  };
+  return pagedCollection(id, items);
 }
 
@@ -6555,5 +6520,5 @@
 
 export default {
-  AP_CONTEXT, getOrCreateKeys, apWants, sendAP, actorId, noteId, stripLeadingMentions,
+  AP_CONTEXT, getOrCreateKeys, apWants, sendAP, actorId, noteId, stripLeadingMentions, pagedCollection,
   buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFollowing, buildFeatured,
   channelUrls, channelCategory, timelineFields, guessMediaType,
Index: src/services/ap-core.js
===================================================================
--- src/services/ap-core.js	(revision cb3001e6d3bf4bb9612f2f29fd9500d493bf84bb)
+++ src/services/ap-core.js	(revision aed0092c4492bca99e1c9a5c82332c1d08fe1bee)
@@ -187,2 +187,52 @@
   return out;
 }
+
+/**
+ * Een AS2-collectie MET de paginavelden erbij (shaer-0nh, 11-8).
+ *
+ * WAAROM DIT EEN HELPER IS EN GEEN REGELS. Funkwhale weigerde onze outbox met
+ * "first: This field is required" en "last: This field is required" -- de eerste
+ * concrete reden die we hoorden waarom er niets van ons binnenkwam. AS2 EIST die
+ * velden niet, maar bijna iedereen pagineert, en een lezer die de paginaweg
+ * volgt liep dood. Toen dat voor de outbox gerepareerd was misten alle andere
+ * collecties ze nog steeds. Een helper zorgt dat de volgende collectie ze niet
+ * opnieuw vergeet.
+ *
+ * DE ITEMS BLIJVEN INLINE op de wortel. Shaer bouwt zijn feed daaruit, en wie
+ * hem vandaag leest hoort er morgen niet voor te hoeven pagineren. Onze
+ * collecties zijn gekapt, dus er is precies EEN pagina en wijzen first en last
+ * naar dezelfde.
+ *
+ * @param {string} id        de collectie-uri, zonder query
+ * @param {Array}  items     wat erin zit (mag leeg)
+ * @param {object} opts
+ *   totalItems  als de telling niet items.length is (followers geeft publiek
+ *               alleen een AANTAL en houdt de lijst dicht)
+ *   page        true -> een OrderedCollectionPage met partOf in plaats van de wortel
+ *   extra       velden die op de wortel horen (attributedTo, shaer:*)
+ */
+export function pagedCollection(id, items, { totalItems, page = false, extra = {} } = {}) {
+  const lijst = items || [];
+  const telling = totalItems === undefined ? lijst.length : totalItems;
+  const eerste = `${id}?page=1`;
+  if (page) {
+    return {
+      '@context': AP_CONTEXT,
+      id: eerste,
+      type: 'OrderedCollectionPage',
+      partOf: id,
+      totalItems: telling,
+      orderedItems: lijst,
+    };
+  }
+  return {
+    '@context': AP_CONTEXT,
+    id,
+    type: 'OrderedCollection',
+    ...extra,
+    totalItems: telling,
+    first: eerste,
+    last: eerste,
+    orderedItems: lijst,
+  };
+}
Index: src/services/guardianship/queues.js
===================================================================
--- src/services/guardianship/queues.js	(revision cb3001e6d3bf4bb9612f2f29fd9500d493bf84bb)
+++ src/services/guardianship/queues.js	(revision aed0092c4492bca99e1c9a5c82332c1d08fe1bee)
@@ -20,6 +20,12 @@
 import * as handshake from './handshake.js';
 
+// De @context zet de route erop (queueRoute), dus hier bewust niet
+// pagedCollection uit ap-core -- die voegt hem toe en dan staat hij er twee
+// keer. Wel dezelfde paginavelden, om dezelfde reden: een lezer die de
+// paginaweg volgt hoort niet dood te lopen (Funkwhale, 11-8).
 const collection = (id, items) => ({
-  id, type: 'OrderedCollection', totalItems: items.length, orderedItems: items,
+  id, type: 'OrderedCollection', totalItems: items.length,
+  first: `${id}?page=1`, last: `${id}?page=1`,
+  orderedItems: items,
 });
 
Index: src/services/music/index.js
===================================================================
--- src/services/music/index.js	(revision cb3001e6d3bf4bb9612f2f29fd9500d493bf84bb)
+++ src/services/music/index.js	(revision aed0092c4492bca99e1c9a5c82332c1d08fe1bee)
@@ -14,5 +14,5 @@
 
 import db from '../../config/database.js';
-import { AP_CONTEXT, PUBLIC, actorId, noteId, safeUrl, guessMediaType, buildHashtagList } from '../ap-core.js';
+import { AP_CONTEXT, PUBLIC, actorId, noteId, safeUrl, guessMediaType, buildHashtagList, pagedCollection } from '../ap-core.js';
 import { afleidenUitInsluitingen, ingeslotenPlaylists } from '../../assets/js/shared/post-music-type.js';
 
@@ -201,16 +201,8 @@
 /** De collectie van alle open tracks van een site (shaer-0nh, stap 3). */
 export function buildTrackCollection(base, site, rows) {
-  return {
-    '@context': AP_CONTEXT,
-    id: `${actorId(base, site.slug)}/tracks`,
-    type: 'OrderedCollection',
-    attributedTo: actorId(base, site.slug),
-    totalItems: (rows || []).length,
-    // Eén zoekopdracht voor alle rijen samen; zie trackHostPosts.
-    orderedItems: (() => {
-      const posts = site.id ? trackHostPosts(site.id) : null;
-      return (rows || []).map((r) => buildTrackAudio(base, site, r, { hostPosts: posts }));
-    })(),
-  };
+  // 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) } });
 }
 
@@ -270,12 +262,5 @@
     return stub;
   });
-  return {
-    '@context': AP_CONTEXT,
-    id: colId,
-    type: 'OrderedCollection',
-    attributedTo: actorId(base, site.slug),
-    totalItems: items.length,
-    orderedItems: items,
-  };
+  return pagedCollection(colId, items, { extra: { attributedTo: actorId(base, site.slug) } });
 }
 
@@ -289,13 +274,7 @@
   const hostPosts = site.id ? trackHostPosts(site.id) : null;
   const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { coverFallback: playlist.cover_url || null, hostPosts }));
-  const out = {
-    '@context': AP_CONTEXT,
-    id: `${actorId(base, site.slug)}/playlists/${playlist.id}`,
-    type: 'OrderedCollection',
-    name: playlist.title,
-    attributedTo: actorId(base, site.slug),
-    totalItems: items.length,
-    orderedItems: items,
-  };
+  const out = pagedCollection(`${actorId(base, site.slug)}/playlists/${playlist.id}`, items, {
+    extra: { name: playlist.title, attributedTo: actorId(base, site.slug) },
+  });
   // Album of playlist is presentatie; op de draad is het één samenvattingsveld.
   const parts = [];
@@ -449,12 +428,7 @@
 
   const hostPosts = new Map(rows.map((r) => [r.id, { id: post.id, slug: post.slug }]));
-  const out = {
-    '@context': AP_CONTEXT,
-    id: postTracksId(base, site, post.id),
-    type: 'OrderedCollection',
-    attributedTo: actorId(base, site.slug),
-    totalItems: rows.length,
-    orderedItems: rows.map((r) => buildTrackAudio(base, site, r, { hostPosts })),
-  };
+  const out = pagedCollection(postTracksId(base, site, post.id),
+    rows.map((r) => buildTrackAudio(base, site, r, { hostPosts })),
+    { extra: { attributedTo: actorId(base, site.slug) } });
   return leenVanPost(base, site, out, post);
 }
Index: test/ap-outbox-paging.test.js
===================================================================
--- test/ap-outbox-paging.test.js	(revision cb3001e6d3bf4bb9612f2f29fd9500d493bf84bb)
+++ test/ap-outbox-paging.test.js	(revision aed0092c4492bca99e1c9a5c82332c1d08fe1bee)
@@ -62,2 +62,38 @@
   assert.equal(leeg.last, `${OUT}?page=1`);
 });
+
+// ── En de andere collecties, want compleetheid was de opdracht ────────────
+
+test('followers en following dragen ze ook, ook count-only', () => {
+  // Publiek geven die alleen een AANTAL en houden de lijst dicht. Juist dan is
+  // een geldige collectie belangrijk: anders is "ik mag de lijst niet zien"
+  // niet te onderscheiden van een kapot antwoord.
+  const f = AP.buildFollowers(BASE, site, 42, null);
+  assert.equal(f.totalItems, 42);
+  assert.deepEqual(f.orderedItems, []);
+  assert.equal(f.first, `${BASE}/ap/users/dev/followers?page=1`);
+  const g = AP.buildFollowing(BASE, site, 3, null);
+  assert.equal(g.first, `${BASE}/ap/users/dev/following?page=1`);
+  assert.equal(g.last, `${BASE}/ap/users/dev/following?page=1`);
+});
+
+test('featured ook', () => {
+  const ft = AP.buildFeatured(BASE, site, [POST]);
+  assert.equal(ft.first, `${BASE}/ap/users/dev/featured?page=1`);
+  assert.equal(ft.orderedItems.length, 1, 'en de items blijven inline');
+});
+
+test('de muziekcollecties ook, met hun eigen velden erbij', () => {
+  const tc = AP.buildTrackCollection(BASE, site, []);
+  assert.equal(tc.first, `${BASE}/ap/users/dev/tracks?page=1`);
+  assert.equal(tc.attributedTo, `${BASE}/ap/users/dev`,
+    'de helper mag attributedTo niet opeten');
+});
+
+test('de guardianship-wachtrijen ook', async () => {
+  const q = await import('../src/services/guardianship/queues.js');
+  const c = q.helpCollection('https://ons.test/q/help', 'dev');
+  assert.equal(c.first, 'https://ons.test/q/help?page=1');
+  assert.equal(c['@context'], undefined,
+    'de context zet de route erop -- twee keer zou hem ongeldig maken');
+});
