Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision fc41e043cd09fae43cf51adae8a1d3c81f00d619)
+++ src/routes/activitypub.js	(revision cb3001e6d3bf4bb9612f2f29fd9500d493bf84bb)
@@ -182,5 +182,5 @@
   });
   if (audience === 'blocked') {
-    return AP.sendAP(res, AP.buildOutbox(baseUrl(req), site, []), 'private, no-store');
+    return AP.sendAP(res, AP.buildOutbox(baseUrl(req), site, [], [], { page: !!req.query.page }), 'private, no-store');
   }
   const fanClause = audience === 'friend' ? '' : "AND (fan_only IS NULL OR fan_only = 0)";
@@ -192,5 +192,5 @@
   // De tracks gaan mee voor iedereen die de deur door mag; de blocked-tak
   // hierboven levert bewust een outbox ZONDER posts en zonder tracks.
-  const ob = AP.buildOutbox(baseUrl(req), site, posts, AP.siteOpenTracks(site.id));
+  const ob = AP.buildOutbox(baseUrl(req), site, posts, AP.siteOpenTracks(site.id), { page: !!req.query.page });
   if (audience === 'friend') {
     // The owner's app builds its feed from this leg, and every note here is
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision fc41e043cd09fae43cf51adae8a1d3c81f00d619)
+++ src/services/ActivityPubService.js	(revision cb3001e6d3bf4bb9612f2f29fd9500d493bf84bb)
@@ -1019,5 +1019,5 @@
  * zijn eigen database bevraagt zou dwars door die deur heen leveren.
  */
-export function buildOutbox(base, site, posts, tracks = []) {
+export function buildOutbox(base, site, posts, tracks = [], { page = false } = {}) {
   const id = `${actorId(base, site.slug)}/outbox`;
   const wanneer = (x) => Date.parse(x && x.published ? x.published : 0) || 0;
@@ -1032,4 +1032,26 @@
     .sort((a, b) => wanneer(b) - wanneer(a))
     .slice(0, MAX_OUTBOX);
+  // GEPAGINEERD, ook al past alles op een pagina (Funkwhale, 11-8).
+  //
+  // Hun serializer weigerde onze outbox met "first: This field is required" en
+  // "last: This field is required". AS2 EIST ze niet -- een collectie mag zijn
+  // items inline dragen -- maar bijna iedereen pagineert, en een lezer die de
+  // paginaweg volgt liep hier dood. Dit is de eerste concrete reden die we
+  // hoorden waarom er niets van ons binnenkwam.
+  //
+  // De items blijven WEL inline op de wortel. Shaer bouwt zijn feed daaruit, en
+  // 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,
@@ -1037,4 +1059,6 @@
     type: 'OrderedCollection',
     totalItems: items.length,
+    first: eerste,
+    last: eerste,
     orderedItems: items,
   };
