Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision 7e53594562c219f7ca4b824fb1257c4526b43783)
+++ src/routes/activitypub.js	(revision 0db3c72bed823ab28b6f74eedfed3a1cf11faf98)
@@ -214,8 +214,18 @@
       if (it && it.object && typeof it.object === 'object') {
         it.object['shaer:author'] = me;
+        // En de standaardvorm ernaast (shaer-nmw): de ingesloten actor, zodat
+        // ook een generieke lezer de byline heeft.
+        it.object.attributedTo = AP.actorObject(
+          (typeof it.object.attributedTo === 'string' ? it.object.attributedTo : undefined) || AP.actorId(baseUrl(req), site.slug),
+          me,
+        );
         const row = byNote.get(it.object.id);
         if (row) {
           it.object['shaer:quote'] = AP.timelineQuote(row.quote_json);
-          if (bearerEmbeds) it.object['shaer:embed'] = AP.timelineEmbed(row.embed_json, { playback: bearerEmbeds.playback });
+          it.object.quote = AP.quoteObject(row.quote_json);
+          if (bearerEmbeds) {
+            it.object['shaer:embed'] = AP.timelineEmbed(row.embed_json, { playback: bearerEmbeds.playback });
+            it.object.preview = AP.previewObject(row.embed_json, { playback: bearerEmbeds.playback });
+          }
         }
       }
@@ -438,4 +448,18 @@
   // goed van een vreemde server. De naam zelf blijft, met :shortcode: als tekst.
   const gateAuthor = (a) => (a && !emojiAllowed ? { ...a, emojis: undefined } : a);
+  // ── Standaardvormen naast het dialect (shaer-nmw) ────────────────
+  //
+  // Een lezer die AS2 kent heeft nu genoeg aan attributedTo (ingesloten
+  // actor), quote (FEP-044f als object), preview (AS2 core) en de
+  // Announce-wrapper. De shaer:-velden blijven er nog naast staan voor apps
+  // in het veld; die gaan eruit als de clients om zijn.
+  const authorInfo = (r, p) => {
+    const info = {
+      name: r[`${p}name`] || undefined, handle: r[`${p}handle`] || undefined,
+      icon: r[`${p}icon`] || undefined, url: r[`${p}url`] || undefined,
+      emojis: (() => { try { return r[`${p}emoji_json`] ? JSON.parse(r[`${p}emoji_json`]) : undefined; } catch { return undefined; } })(),
+    };
+    return (info.name || info.handle || info.icon) ? gateAuthor(info) : undefined;
+  };
   const rows = AP.getTimeline(auth.site.slug, 60);
   // Eén query voor de hele pagina (shaer-9e9 fase 2): shaer:liked komt uit de
@@ -443,13 +467,22 @@
   // ap_timeline. Per rij vragen zou hier een N+1 opleveren.
   const reacties = AP.getReactionsFor(auth.site.slug, rows.map((t) => t.id));
-  const posts = rows.map((t) => ({
+  const posts = rows.map((t) => {
+    const auteur = authorInfo(t, 'author_');
+    const booster = authorInfo(t, 'reblog_');
+    const boosterUri = t.reblog_url || t.reblog_handle || undefined;
+    return {
     id: `${t.id}#create`,
-    type: 'Create',
-    actor: t.author_uri,
+    // EEN BOOST IS EEN ANNOUNCE (shaer-nmw): een Create met een
+    // zijkanaal-property was onze uitvinding; de wrapper is de standaard, en
+    // elke AP-client leest hem al.
+    type: booster ? 'Announce' : 'Create',
+    actor: booster ? (AP.actorObject(boosterUri || t.author_uri, booster)) : t.author_uri,
     published: t.published || t.created_at || undefined,
     object: {
       id: t.id,
       type: 'Note',
-      attributedTo: t.author_uri,
+      // AS2 staat een INGESLOTEN actor toe; dan heeft elke client de byline,
+      // niet alleen de onze (shaer-nmw).
+      attributedTo: AP.actorObject(t.author_uri, auteur),
       content: t.content,
       url: t.url || undefined,
@@ -497,6 +530,11 @@
       // Carries shaer:playerUrl only when the playback gate is open too.
       'shaer:embed': embedsAllowed ? AP.timelineEmbed(t.embed_json, { playback: playbackAllowed }) : undefined,
+      // De standaardvormen (shaer-nmw): FEP-044f quote als object, AS2 preview
+      // als kaart. Dezelfde poorten als hun shaer:-tegenhangers hierboven.
+      quote: quotesAllowed ? AP.quoteObject(t.quote_json) : undefined,
+      preview: embedsAllowed ? AP.previewObject(t.embed_json, { playback: playbackAllowed }) : undefined,
     },
-  }));
+  };
+  });
   // The direct notes addressed to this account: a plain DM, a guardian's wave
   // (§5), a ward's 🛟 help request (§5.2.1). Those are messages, not posts, so
@@ -520,5 +558,9 @@
       id: m.object_uri,
       type: 'Note',
-      attributedTo: m.actor_uri,
+      attributedTo: AP.actorObject(m.actor_uri, (m.actor_name || m.actor_handle || m.actor_icon) ? gateAuthor({
+        name: m.actor_name || undefined, handle: m.actor_handle || undefined,
+        icon: m.actor_icon || undefined, url: m.actor_url || undefined,
+        emojis: (() => { try { return m.actor_emoji_json ? JSON.parse(m.actor_emoji_json) : undefined; } catch { return undefined; } })(),
+      }) : undefined),
       content: AP.stripLeadingMentions(m.content),
       url: m.note_url || undefined,
@@ -543,4 +585,6 @@
       } : undefined),
       'shaer:embed': embedsAllowed ? AP.timelineEmbed(m.embed_json, { playback: playbackAllowed }) : undefined,
+      quote: quotesAllowed ? AP.quoteObject(m.quote_json) : undefined,
+      preview: embedsAllowed ? AP.previewObject(m.embed_json, { playback: playbackAllowed }) : undefined,
     },
   }));
@@ -557,5 +601,9 @@
       id: m.object_uri,
       type: 'Note',
-      attributedTo: m.actor_uri,
+      attributedTo: AP.actorObject(m.actor_uri, (m.actor_name || m.actor_handle || m.actor_icon) ? gateAuthor({
+        name: m.actor_name || undefined, handle: m.actor_handle || undefined,
+        icon: m.actor_icon || undefined, url: m.actor_url || undefined,
+        emojis: (() => { try { return m.actor_emoji_json ? JSON.parse(m.actor_emoji_json) : undefined; } catch { return undefined; } })(),
+      }) : undefined),
       content: AP.stripLeadingMentions(m.content),
       inReplyTo: m.parent_uri || `${base}/ap/notes/${m.post_id}`,
@@ -571,4 +619,6 @@
       } : undefined,
       'shaer:embed': embedsAllowed ? AP.timelineEmbed(m.embed_json, { playback: playbackAllowed }) : undefined,
+      quote: quotesAllowed ? AP.quoteObject(m.quote_json) : undefined,
+      preview: embedsAllowed ? AP.previewObject(m.embed_json, { playback: playbackAllowed }) : undefined,
     },
   }));
@@ -585,5 +635,9 @@
     // The leading mention anchor is addressing, not prose (the DM leg strips
     // it the same way); the Mention tags built from the full content stay.
-    object: { ...n, content: AP.stripLeadingMentions(n.content), 'shaer:author': mine },
+    object: {
+      ...n, content: AP.stripLeadingMentions(n.content),
+      'shaer:author': mine,
+      attributedTo: AP.actorObject(typeof n.attributedTo === 'string' ? n.attributedTo : me, mine),
+    },
   }));
   // Newest first over all legs, so the app can keep treating this as one feed.
@@ -905,4 +959,6 @@
     'shaer:quote': AP.timelineQuote(uit.quoteJson),
     'shaer:embed': embedsAllowed ? AP.timelineEmbed(uit.embedJson, { playback }) : undefined,
+    quote: AP.quoteObject(uit.quoteJson),
+    preview: embedsAllowed ? AP.previewObject(uit.embedJson, { playback }) : undefined,
   }, 'private, no-store');
 });
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 7e53594562c219f7ca4b824fb1257c4526b43783)
+++ src/services/ActivityPubService.js	(revision 0db3c72bed823ab28b6f74eedfed3a1cf11faf98)
@@ -4808,4 +4808,96 @@
 
 /** The stored external-embed card, for the C2S read. */
+// ── Standaardvormen in plaats van eigen dialect (shaer-nmw) ───────
+//
+// Robins waarschuwing: geen Klonkt/Shaer-dialect schrijven waar AS2 of een FEP
+// het al regelt. Vier eigen properties hadden een standaard naast zich staan,
+// en deze helpers zijn die standaard -- een definitie per vorm, zodat de tien
+// plekken die ze emitten niet elk hun eigen variant krijgen.
+//
+// De oude shaer:-velden blijven er voorlopig NAAST staan. Een app in het veld
+// leest ze nog, en een leeg scherm is een duurdere fout dan een dubbel veld;
+// ze gaan eruit als de clients om zijn (tweede helft van shaer-nmw).
+
+/** FEP-9098 Emoji-tags uit een {shortcode: url}-kaart. */
+function emojiTagsFromMap(emojis) {
+  const uit = Object.entries(emojis || {})
+    .filter(([naam, url]) => naam && url)
+    .map(([naam, url]) => ({ type: 'Emoji', name: naam, icon: { type: 'Image', url } }));
+  return uit.length ? uit : undefined;
+}
+
+/**
+ * Een actor als INGESLOTEN OBJECT voor `attributedTo` / `actor`.
+ *
+ * AS2 staat toe dat attributedTo een object is in plaats van een URI, en dan
+ * heeft ELKE client er wat aan -- niet alleen de onze, die er shaer:author
+ * naast kreeg. `preferredUsername` is de lokale naam; een lezer leidt de handle
+ * af uit die naam plus de host van de id, precies zoals wij serverkant ook
+ * doen. Weten we niets van de persoon, dan blijft het de kale URI: een leeg
+ * object zou beweren dat we hem kennen.
+ */
+export function actorObject(uri, info) {
+  if (!uri) return undefined;
+  const iets = info && (info.name || info.handle || info.icon || info.url);
+  if (!iets) return uri;
+  const o = { id: uri, type: 'Person' };
+  if (info.name) o.name = info.name;
+  const lokaal = String(info.handle || '').replace(/^@/, '').split('@')[0];
+  if (lokaal) o.preferredUsername = lokaal;
+  if (info.icon) o.icon = { type: 'Image', url: info.icon };
+  if (info.url) o.url = info.url;
+  const tags = emojiTagsFromMap(info.emojis);
+  if (tags) o.tag = tags;
+  return o;
+}
+
+/**
+ * De linkkaart als AS2 `preview` (core: "identifies an entity that provides a
+ * preview of this object"). Een Page met url, name en image IS een kaart; daar
+ * hoefde shaer:embed nooit voor te bestaan.
+ *
+ * Wat WEL van ons blijft is de spelerpagina: dat die alleen meegaat als de
+ * guardians de poort openden is FEP-633c-gedrag en heeft geen AS2-tegenhanger.
+ */
+export function previewObject(embedJson, { playback = false } = {}) {
+  const e = timelineEmbed(embedJson, { playback });
+  if (!e) return undefined;
+  const thumb = (e.media || []).find((m) => m && m.url);
+  const p = { type: 'Page', url: e.url };
+  if (e.title) p.name = e.title;
+  if (thumb) p.image = { type: 'Image', url: thumb.url };
+  if (e.author && (e.author.name || e.author.handle)) {
+    p.attributedTo = { type: 'Person', name: e.author.name || e.author.handle };
+  }
+  if (e['shaer:playerUrl']) p['shaer:playerUrl'] = e['shaer:playerUrl'];
+  if (e['shaer:playable']) p['shaer:playable'] = e['shaer:playable'];
+  return p;
+}
+
+/**
+ * De geciteerde post als OBJECT in `quote` (FEP-044f staat toe dat quote het
+ * object zelf is, niet alleen een URI). De opgeslagen momentopname wordt hier
+ * een echte Note, met de auteur als ingesloten actor -- dus geen tweede eigen
+ * property voor iets dat de FEP al kan.
+ */
+export function quoteObject(quoteJson) {
+  const q = timelineQuote(quoteJson);
+  if (!q) return undefined;
+  const note = { type: 'Note', id: q.url, url: q.url };
+  if (q.content) note.content = q.content;
+  if (q.published) note.published = q.published;
+  if (q.author) {
+    note.attributedTo = actorObject(q.author.url || q.url, {
+      name: q.author.name, handle: q.author.handle, icon: q.author.icon,
+    });
+  }
+  const media = (q.media || []).filter((m) => m && m.url)
+    .map((m) => ({ type: 'Document', mediaType: m.type || undefined, url: m.url }));
+  if (media.length) note.attachment = media;
+  const tags = emojiTagsFromMap(q.emojis);
+  if (tags) note.tag = tags;
+  return note;
+}
+
 export function timelineEmbed(embedJson, { playback = false } = {}) {
   try {
@@ -6188,5 +6280,5 @@
   getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
   deliverWithRetry, enqueueDelivery, processDeliveryQueue, startDeliveryWorker,
-  sendMaybe304, etagFor, onGuardian, wakeGuardian, onGuardianshipEvent, proposeGate, getReplyUris, getThread, filterThreadToCircle, gateAttachments, stripEmojiTags, markNotificationsSeen, countUnseenNotifications, hasPlayableAudio,
+  sendMaybe304, etagFor, onGuardian, wakeGuardian, onGuardianshipEvent, proposeGate, getReplyUris, getThread, filterThreadToCircle, gateAttachments, stripEmojiTags, actorObject, previewObject, quoteObject, markNotificationsSeen, countUnseenNotifications, hasPlayableAudio,
   linkifyBody, bakePostContent, bakePostContentWithMentions, listFollowers, removeFollower, listConnections,
   noteVisibility, belongsInTimeline, playerUrlFor, isRejectedObject, rejectInteraction, interactionReportTarget,
Index: test/standard-shapes.test.js
===================================================================
--- test/standard-shapes.test.js	(revision 0db3c72bed823ab28b6f74eedfed3a1cf11faf98)
+++ test/standard-shapes.test.js	(revision 0db3c72bed823ab28b6f74eedfed3a1cf11faf98)
@@ -0,0 +1,84 @@
+// Standaardvormen in plaats van eigen dialect (shaer-nmw).
+//
+// Robins waarschuwing: geen Klonkt/Shaer-dialect schrijven waar AS2 of een FEP
+// het al regelt. Deze toetsen zijn het criterium uit die bead -- niet "onze app
+// snapt het", maar: HEEFT EEN GENERIEKE AP-LEZER ER IETS AAN. Daarom kijken ze
+// alleen naar standaardvelden en nooit naar een shaer:-property.
+//
+// In-memory SQLite. Run: npm test
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+
+process.env.DATABASE_PATH = ':memory:';
+process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
+
+const dbMod = await import('../src/config/database.js');
+dbMod.initializeDatabase();
+const AP = await import('../src/services/ActivityPubService.js');
+
+test('attributedTo draagt de actor als object, zodat elke client een byline heeft', () => {
+  const uit = AP.actorObject('https://elders/u/tante', {
+    name: 'Tante Til', handle: '@til@elders', icon: 'https://elders/til.png',
+    url: 'https://elders/@til', emojis: { ':zwaai:': 'https://elders/zwaai.png' },
+  });
+  assert.equal(uit.id, 'https://elders/u/tante');
+  assert.equal(uit.type, 'Person');
+  assert.equal(uit.name, 'Tante Til');
+  // De LOKALE naam, zoals AS2 hem bedoelt; de handle leidt een lezer af uit
+  // preferredUsername plus de host van de id.
+  assert.equal(uit.preferredUsername, 'til');
+  assert.deepEqual(uit.icon, { type: 'Image', url: 'https://elders/til.png' });
+  // FEP-9098 hoort in tag, niet in een eigen emoji-kaart.
+  assert.equal(uit.tag[0].type, 'Emoji');
+  assert.equal(uit.tag[0].name, ':zwaai:');
+});
+
+test('weten we niets van de persoon, dan blijft attributedTo de kale URI', () => {
+  // Een leeg object zou beweren dat we hem kennen. Een URI is eerlijk en is
+  // bovendien wat elke AP-server verwacht.
+  assert.equal(AP.actorObject('https://elders/u/onbekend', undefined), 'https://elders/u/onbekend');
+  assert.equal(AP.actorObject('https://elders/u/onbekend', {}), 'https://elders/u/onbekend');
+});
+
+test('een linkkaart is een AS2 preview met url, name en image', () => {
+  const embed = JSON.stringify({
+    url: 'https://nieuws.example/artikel', title: 'Een mooi artikel',
+    author: { name: 'De Krant' }, media: [{ url: 'https://nieuws.example/p.jpg' }],
+  });
+  const p = AP.previewObject(embed, { playback: false });
+  assert.equal(p.type, 'Page');
+  assert.equal(p.url, 'https://nieuws.example/artikel');
+  assert.equal(p.name, 'Een mooi artikel');
+  assert.deepEqual(p.image, { type: 'Image', url: 'https://nieuws.example/p.jpg' });
+  assert.equal(p.attributedTo.name, 'De Krant');
+  // De spelerpagina blijft van ons: die hangt aan de guardian-poort en heeft
+  // geen AS2-tegenhanger. Dicht = niet aanwezig.
+  assert.ok(!p['shaer:playerUrl']);
+});
+
+test('een quote is de geciteerde Note zelf (FEP-044f), met ingesloten auteur', () => {
+  const snapshot = JSON.stringify({
+    url: 'https://elders/notes/1',
+    author: { name: 'Til', handle: '@til@elders', icon: 'https://elders/til.png' },
+    content: '<p>hallo</p>', published: '2026-08-01T10:00:00Z',
+    media: [{ url: 'https://elders/foto.jpg', type: 'image/jpeg' }],
+  });
+  const q = AP.quoteObject(snapshot);
+  assert.equal(q.type, 'Note');
+  assert.equal(q.id, 'https://elders/notes/1');
+  assert.equal(q.content, '<p>hallo</p>');
+  assert.equal(q.published, '2026-08-01T10:00:00Z');
+  // De auteur van het citaat is zelf ook een ingesloten actor, niet een
+  // losse naam-kaart.
+  assert.equal(q.attributedTo.type, 'Person');
+  assert.equal(q.attributedTo.name, 'Til');
+  assert.equal(q.attributedTo.preferredUsername, 'til');
+  assert.deepEqual(q.attachment, [{ type: 'Document', mediaType: 'image/jpeg', url: 'https://elders/foto.jpg' }]);
+});
+
+test('niets te tonen levert niets op, geen leeg omhulsel', () => {
+  assert.equal(AP.previewObject(null), undefined);
+  assert.equal(AP.previewObject('{}'), undefined);
+  assert.equal(AP.quoteObject(null), undefined);
+  assert.equal(AP.quoteObject('rommel'), undefined);
+});
