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,
   };
Index: test/ap-outbox-paging.test.js
===================================================================
--- test/ap-outbox-paging.test.js	(revision cb3001e6d3bf4bb9612f2f29fd9500d493bf84bb)
+++ test/ap-outbox-paging.test.js	(revision cb3001e6d3bf4bb9612f2f29fd9500d493bf84bb)
@@ -0,0 +1,63 @@
+// De outbox pagineert (Funkwhale, 11-8).
+//
+// Hun serializer weigerde onze outbox letterlijk met
+//   {'first': ['This field is required.'], 'last': ['This field is required.']}
+// AS2 eist die velden niet -- een collectie mag zijn items inline dragen -- maar
+// bijna iedereen pagineert, en dit was de eerste CONCRETE reden die we hoorden
+// waarom er niets van ons binnenkwam.
+//
+// De items blijven inline op de wortel: Shaer bouwt zijn feed daaruit.
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+
+process.env.DATABASE_PATH = ':memory:';
+process.env.PUBLIC_BASE_URL = 'https://ons.test';
+
+const dbMod = await import('../src/config/database.js');
+const db = dbMod.default;
+dbMod.initializeDatabase();
+const AP = await import('../src/services/ActivityPubService.js');
+
+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) VALUES (?,?,?,?)').run('s1', 'dev', 'Dev', 'u1');
+const site = db.prepare("SELECT * FROM sites WHERE id = 's1'").get();
+const POST = { id: 'p1', slug: 'hallo', title: 'Hallo', content: '<p>x</p>', published_at: '2026-08-01T00:00:00Z' };
+const BASE = 'https://ons.test';
+const OUT = `${BASE}/ap/users/dev/outbox`;
+
+test('de wortel draagt first en last', () => {
+  const ob = AP.buildOutbox(BASE, site, [POST]);
+  assert.equal(ob.type, 'OrderedCollection');
+  assert.equal(ob.first, `${OUT}?page=1`);
+  assert.equal(ob.last, `${OUT}?page=1`);
+});
+
+test('en houdt zijn items inline, want Shaer leest die', () => {
+  const ob = AP.buildOutbox(BASE, site, [POST]);
+  assert.equal(ob.orderedItems.length, 1);
+  assert.equal(ob.totalItems, 1);
+});
+
+test('de pagina is een OrderedCollectionPage die terugwijst', () => {
+  const pg = AP.buildOutbox(BASE, site, [POST], [], { page: true });
+  assert.equal(pg.type, 'OrderedCollectionPage');
+  assert.equal(pg.id, `${OUT}?page=1`);
+  assert.equal(pg.partOf, OUT, 'zonder partOf hangt een pagina in de lucht');
+  assert.equal(pg.orderedItems.length, 1);
+});
+
+test('een pagina noemt zichzelf geen wortel', () => {
+  const pg = AP.buildOutbox(BASE, site, [POST], [], { page: true });
+  assert.equal(pg.first, undefined, 'first hoort op de wortel, niet op de pagina');
+  assert.equal(pg.last, undefined);
+});
+
+test('ook een LEGE outbox draagt ze -- een geblokkeerde lezer krijgt geldige AS2', () => {
+  // De blocked-tak levert een lege collectie. Zonder first/last zou juist die
+  // door dezelfde serializer geweigerd worden.
+  const leeg = AP.buildOutbox(BASE, site, []);
+  assert.equal(leeg.totalItems, 0);
+  assert.equal(leeg.first, `${OUT}?page=1`);
+  assert.equal(leeg.last, `${OUT}?page=1`);
+});
