Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision 8f8b40fc0285013c26499b06cfc6dc5762af9abd)
+++ src/config/database.js	(revision eb36688598f8d808a62a4fd9336d9bbdf02cbf9f)
@@ -548,4 +548,5 @@
   ensureColumn('ap_timeline', 'cw', 'TEXT');                 // remote content-warning text
   ensureColumn('ap_timeline', 'emoji_json', 'TEXT');         // FEP-9098 custom emoji Emoji tags from the inbound note, served back as `tag`
+  ensureColumn('ap_timeline', 'link_json', 'TEXT');          // FEP-e232 object-link (quote/ref) tags from the inbound note, served back as `tag`
   ensureColumn('ap_timeline', 'reblog_name', 'TEXT');        // a followed account boosted this → "X boosted"
   ensureColumn('ap_timeline', 'reblog_handle', 'TEXT');      //   the booster's @handle
Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision 8f8b40fc0285013c26499b06cfc6dc5762af9abd)
+++ src/routes/activitypub.js	(revision eb36688598f8d808a62a4fd9336d9bbdf02cbf9f)
@@ -162,7 +162,12 @@
       // client renders their images/audio like own outbox posts.
       attachment: AP.timelineAttachments(t.media_json),
-      // FEP-9098 custom emojis: the note's Emoji tags, so the client can
-      // render :shortcode: as an image instead of literal text.
-      tag: AP.timelineEmojis(t.emoji_json),
+      // The note's preserved tags, so the client can render them: FEP-9098
+      // Emoji tags (:shortcode: → image) and FEP-e232 Link tags (quotes /
+      // inline object references). Combined into one `tag` array; omitted
+      // when the note has neither.
+      tag: (() => {
+        const tags = [...(AP.timelineEmojis(t.emoji_json) || []), ...(AP.timelineObjectLinks(t.link_json) || [])];
+        return tags.length ? tags : undefined;
+      })(),
     },
   }));
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 8f8b40fc0285013c26499b06cfc6dc5762af9abd)
+++ src/services/ActivityPubService.js	(revision eb36688598f8d808a62a4fd9336d9bbdf02cbf9f)
@@ -1539,4 +1539,6 @@
           // FEP-9098: keep the note's custom-emoji tags so the C2S inbox read can serve them.
           { const ej = extractEmojiTags(o.tag); if (ej) { try { db.prepare('UPDATE ap_timeline SET emoji_json = ? WHERE id = ? AND slug = ?').run(ej, o.id, s.slug); } catch { /* ignore */ } } }
+          // FEP-e232: keep the note's object-link (quote/ref) tags for the same read.
+          { const lj = extractObjectLinkTags(o.tag); if (lj) { try { db.prepare('UPDATE ap_timeline SET link_json = ? WHERE id = ? AND slug = ?').run(lj, o.id, s.slug); } catch { /* ignore */ } } }
           if (poll) { try { db.prepare('UPDATE ap_timeline SET poll_json = ? WHERE id = ? AND slug = ?').run(JSON.stringify(poll), o.id, s.slug); } catch { /* ignore */ } }
         }
@@ -2582,4 +2584,25 @@
 }
 
+// FEP-e232 object links (quotes / inline references). Inbound: keep the note's
+// Link tags whose mediaType marks an AP object (the AS2-profiled ld+json, or
+// activity+json as its equivalent) as JSON, so the C2S inbox read can serve
+// them back and a client (Shaer) can render the quote/reference. Mirrors
+// extractEmojiTags. Plain hyperlinks (text/html) and Mentions are dropped.
+export function extractObjectLinkTags(tag) {
+  const arr = Array.isArray(tag) ? tag : (tag ? [tag] : []);
+  const links = arr.filter((t) => {
+    if (!t || (Array.isArray(t.type) ? t.type[0] : t.type) !== 'Link') return false;
+    if (typeof t.href !== 'string' || !t.href) return false;
+    const mt = String(t.mediaType || '').toLowerCase();
+    return (mt.startsWith('application/ld+json') && mt.includes('activitystreams'))
+      || mt.startsWith('application/activity+json');
+  });
+  return links.length ? JSON.stringify(links) : null;
+}
+export function timelineObjectLinks(linkJson) {
+  try { const arr = linkJson ? JSON.parse(linkJson) : null; return (Array.isArray(arr) && arr.length) ? arr : undefined; }
+  catch { return undefined; }
+}
+
 // ── Cirkel = posts from the accounts you auto-boost ("feature an artist") ──
 let _abCount, _cirkelPosts, _cirkelMembers;
@@ -2678,5 +2701,5 @@
 // during a flux window, e.g. a fleet-wide update), and drops notes that are gone
 // (404/410). Bump SELFHEAL_VERSION only on a release that warrants a re-sync.
-const SELFHEAL_VERSION = 8; // v8: also re-capture FEP-9098 custom-emoji tags (emoji_json) onto already-cached posts
+const SELFHEAL_VERSION = 9; // v9: also re-capture FEP-e232 object-link tags (link_json) onto already-cached posts
 async function fetchNoteAP(url) {
   try {
@@ -2739,4 +2762,6 @@
         // FEP-9098: keep custom-emoji tags from backfilled posts too.
         { const ej = extractEmojiTags(o.tag); if (ej) { try { db.prepare('UPDATE ap_timeline SET emoji_json = ? WHERE id = ? AND slug = ?').run(ej, o.id, slug); } catch { /* ignore */ } } }
+        // FEP-e232: keep object-link (quote/ref) tags from backfilled posts too.
+        { const lj = extractObjectLinkTags(o.tag); if (lj) { try { db.prepare('UPDATE ap_timeline SET link_json = ? WHERE id = ? AND slug = ?').run(lj, o.id, slug); } catch { /* ignore */ } } }
         // Set poll_json if this is a poll and we don't already have it (COALESCE preserves a vote).
         if (poll) { try { db.prepare('UPDATE ap_timeline SET poll_json = COALESCE(poll_json, ?) WHERE id = ? AND slug = ?').run(JSON.stringify(poll), o.id, slug); } catch { /* ignore */ } }
@@ -2859,5 +2884,5 @@
     if (cur >= SELFHEAL_VERSION) return; // already healed for this version — skip on normal boots
     let rows = [];
-    try { rows = db.prepare('SELECT id, content, media_json, nsfw, cw, url, emoji_json FROM ap_timeline ORDER BY rowid DESC LIMIT 200').all(); } catch { /* no table */ }
+    try { rows = db.prepare('SELECT id, content, media_json, nsfw, cw, url, emoji_json, link_json FROM ap_timeline ORDER BY rowid DESC LIMIT 200').all(); } catch { /* no table */ }
     let healed = 0, failed = 0;
     for (const r of rows) {
@@ -2872,6 +2897,7 @@
         const url = note.url || null;          // re-sync the human url (catches a remote slug rename)
         const emoji = extractEmojiTags(note.tag);   // FEP-9098: re-capture custom-emoji tags (v8)
-        if ((html && html !== r.content) || media !== (r.media_json || '[]') || nsfw !== (r.nsfw || 0) || (cw || '') !== (r.cw || '') || (url && url !== r.url) || (emoji || '') !== (r.emoji_json || '')) {
-          db.prepare('UPDATE ap_timeline SET content = ?, media_json = ?, nsfw = ?, cw = ?, url = COALESCE(?, url), emoji_json = ? WHERE id = ?').run(html || r.content, media, nsfw, cw, url, emoji, r.id);
+        const link = extractObjectLinkTags(note.tag);   // FEP-e232: re-capture object-link tags (v9)
+        if ((html && html !== r.content) || media !== (r.media_json || '[]') || nsfw !== (r.nsfw || 0) || (cw || '') !== (r.cw || '') || (url && url !== r.url) || (emoji || '') !== (r.emoji_json || '') || (link || '') !== (r.link_json || '')) {
+          db.prepare('UPDATE ap_timeline SET content = ?, media_json = ?, nsfw = ?, cw = ?, url = COALESCE(?, url), emoji_json = ?, link_json = ? WHERE id = ?').run(html || r.content, media, nsfw, cw, url, emoji, link, r.id);
           healed++;
         }
@@ -3377,5 +3403,5 @@
   getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, setMyReaction, getMyReactions, buildReplyNote, getOutboxNote, deliverReply, resolveRemoteNote,
   listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote,
-  webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, timelineAttachments, timelineEmojis, sendInteraction, voteOnPoll, voteOnRemotePoll,
+  webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, timelineAttachments, timelineEmojis, timelineObjectLinks, sendInteraction, voteOnPoll, voteOnRemotePoll,
   acceptGatedFollow, rejectGatedFollow, isWardGuardian, sendFollowDecision,
   parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, localMentionSlugs,
Index: test/object-links.test.js
===================================================================
--- test/object-links.test.js	(revision eb36688598f8d808a62a4fd9336d9bbdf02cbf9f)
+++ test/object-links.test.js	(revision eb36688598f8d808a62a4fd9336d9bbdf02cbf9f)
@@ -0,0 +1,32 @@
+// FEP-e232: Klonkt keeps inbound object-link (quote/ref) tags and serves them on the C2S inbox read.
+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');
+dbMod.initializeDatabase();
+const { extractObjectLinkTags, timelineObjectLinks } = await import('../src/services/ActivityPubService.js');
+const AP = { extractObjectLinkTags, timelineObjectLinks };
+
+test('extractObjectLinkTags keeps AS2-profiled ld+json and activity+json Links; drops plain links and mentions', () => {
+  const tag = [
+    { type: 'Link', mediaType: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', href: 'https://s/objects/1', name: '#1374' },
+    { type: 'Link', mediaType: 'application/activity+json', href: 'https://s/objects/2', rel: ['https://misskey-hub.net/ns#_misskey_quote'] },
+    { type: 'Link', mediaType: 'text/html', href: 'https://plain.example/page' },  // plain hyperlink → dropped
+    { type: 'Mention', href: 'https://s/u/x' },
+  ];
+  const json = AP.extractObjectLinkTags(tag);
+  assert.ok(json);
+  const back = AP.timelineObjectLinks(json);
+  assert.equal(back.length, 2);
+  assert.equal(back[0].href, 'https://s/objects/1');
+  assert.equal(back[0].name, '#1374');
+  assert.equal(back[1].mediaType, 'application/activity+json');
+});
+
+test('no object links → null / undefined (nothing served)', () => {
+  assert.equal(AP.extractObjectLinkTags([{ type: 'Link', mediaType: 'text/html', href: 'https://x' }]), null);
+  assert.equal(AP.extractObjectLinkTags([{ type: 'Hashtag', name: '#hi' }]), null);
+  assert.equal(AP.timelineObjectLinks(null), undefined);
+  assert.equal(AP.timelineObjectLinks('not json'), undefined);
+});
