Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 39111c02cd42a6149a332d4b510b61873cbb2522)
+++ src/services/ActivityPubService.js	(revision 971d00b75043758242745bd7dd7e24dd2fef68bf)
@@ -627,4 +627,19 @@
 // outboxId), sorted as one stream. Consecutive likes/boosts on the same post collapse into
 // one grouped item (actors list + count) so activity doesn't drown out conversations.
+/** ap_outbox.attachments ([{url, mediaType, name}]) naar de vorm die note-body
+ *  leest (media_json: [{url, type, name}]). Geeft null bij niets of rommel,
+ *  zodat een kapotte kolom hooguit media kost en niet de hele regel. */
+function outboxMediaJson(attachments) {
+  if (!attachments) return null;
+  try {
+    const list = JSON.parse(attachments);
+    if (!Array.isArray(list) || !list.length) return null;
+    const media = list
+      .filter((a) => a && a.url)
+      .map((a) => ({ url: a.url, type: a.mediaType || a.type || '', name: a.name || undefined }));
+    return media.length ? JSON.stringify(media) : null;
+  } catch { return null; }
+}
+
 export function getMessages(slug, limit, offset) {
   const off = Math.max(0, offset || 0);
@@ -641,4 +656,10 @@
         in_reply_to: m.in_reply_to, post_slug: m.post_slug, content: m.content,
         editable: m.editable, language: m.language, created_at: m.created_at,
+        // Je eigen bericht hoort er hetzelfde uit te zien als dat van een ander:
+        // note-body rendert Berichten, de Krant en de Guardian-PWA, maar leest
+        // media uit media_json met een `type`, terwijl ap_outbox ze als
+        // `attachments` met een `mediaType` bewaart. Zonder deze vertaling kwam
+        // een foto die JIJ meestuurde als kale tekst binnen.
+        media_json: outboxMediaJson(m.attachments),
       });
     }
@@ -3020,5 +3041,5 @@
   // gaat staan (zie threadKey). Zonder die kolom viel een uitwisseling uit
   // elkaar in "Verzonden" en "Gesprekken".
-  return db.prepare('SELECT id, content, to_handle, to_actor, to_actors, post_slug, in_reply_to, language, created_at FROM ap_outbox WHERE site_slug = ? ORDER BY created_at DESC')
+  return db.prepare('SELECT id, content, to_handle, to_actor, to_actors, post_slug, in_reply_to, attachments, language, created_at FROM ap_outbox WHERE site_slug = ? ORDER BY created_at DESC')
     .all(siteSlug).map((r) => { const c = stripLeadingMentions(r.content); return { ...r, content: c, editable: outboxEditableText(c) }; });
 }
Index: test/messages-conversations.test.js
===================================================================
--- test/messages-conversations.test.js	(revision 39111c02cd42a6149a332d4b510b61873cbb2522)
+++ test/messages-conversations.test.js	(revision 971d00b75043758242745bd7dd7e24dd2fef68bf)
@@ -132,2 +132,31 @@
   assert.equal(a, b, 'dezelfde tegenpartij hoort dezelfde sleutel te geven');
 });
+
+test('een eigen bericht met media rendert als een post, niet als kale tekst', async () => {
+  // note-body (gedeeld met de Krant) leest media_json met een `type`; ap_outbox
+  // bewaart attachments met een `mediaType`. Zonder vertaling viel een foto die
+  // JIJ meestuurde weg.
+  const db = (await import('../src/config/database.js')).default;
+  db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
+    .run('mu', 'mu', 'mu@test', 'x', 'god');
+  db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('ms', 'media', 'Media', 'mu');
+  db.prepare(`INSERT INTO ap_outbox (id, site_slug, post_id, post_slug, to_handle, content, attachments, created_at)
+    VALUES ('om1','media','pm1','een-post','@anna@a.test','<p>kijk</p>',?,'2026-08-06 09:00:00')`)
+    .run(JSON.stringify([{ url: '/media/foto.png', mediaType: 'image/png', name: 'foto' }]));
+
+  const t = AP.getMessages('media', 50, 0).find((i) => i.type === 'thread');
+  const sent = t.messages.find((m) => m.type === 'sent');
+  assert.deepEqual(JSON.parse(sent.media_json), [{ url: '/media/foto.png', type: 'image/png', name: 'foto' }]);
+});
+
+test('een eigen bericht zonder of met kapotte media houdt gewoon zijn tekst', async () => {
+  const db = (await import('../src/config/database.js')).default;
+  db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('ms2', 'media2', 'M2', 'mu');
+  db.prepare(`INSERT INTO ap_outbox (id, site_slug, post_id, post_slug, to_handle, content, attachments, created_at)
+    VALUES ('om2','media2','pm2','p','@anna@a.test','<p>a</p>','geen json','2026-08-06 09:00:00'),
+           ('om3','media2','pm3','p','@bo@b.test','<p>b</p>',NULL,'2026-08-06 09:01:00')`).run();
+  const threads = AP.getMessages('media2', 50, 0).filter((i) => i.type === 'thread');
+  const alle = threads.flatMap((t) => t.messages);
+  assert.equal(alle.length, 2, 'geen bericht mag verdwijnen door kapotte media');
+  for (const m of alle) assert.equal(m.media_json, null);
+});
