Index: src/routes/guardian.js
===================================================================
--- src/routes/guardian.js	(revision 2708282985faa68002d3d9051b403c0e12b11dd9)
+++ src/routes/guardian.js	(revision d9ad6c564eccd0e4c23c3d8c2c8a32334f2443bc)
@@ -19,5 +19,6 @@
 import * as Guardianship from '../services/guardianship/index.js';
 import { t as i18nT, resolveLang } from '../services/i18n.js';
-import { injectCspNonce } from '../middleware/render.js';
+import { injectCspNonce, renderNoteBody } from '../middleware/render.js';
+import { emojiName } from '../services/NoteRender.js';
 
 const router = express.Router();
@@ -51,7 +52,15 @@
   const me = AP.actorId(base, site.slug);
   const help = db.prepare(
-    `SELECT object_uri, note_url, actor_uri, actor_name, actor_handle, actor_icon, content, published, created_at
+    `SELECT object_uri, note_url, actor_uri, actor_name, actor_handle, actor_icon, content, published, created_at,
+            emoji_json, actor_emoji_json, media_json, quote_json, embed_json
      FROM ap_mentions WHERE slug = ? AND help_request = 1 ORDER BY created_at DESC LIMIT 50`
-  ).all(site.slug);
+  ).all(site.slug).map((h) => ({
+    ...h,
+    // The dashboard is built in the browser, so it gets the body finished: the
+    // same partial de Krant and Berichten use. A 🛟 often carries a screenshot
+    // and a link to the post it is about; both belong in the card.
+    body_html: renderNoteBody(h, L),
+    name_html: emojiName(h.actor_name || '', h.actor_emoji_json),
+  }));
   return {
     site: site.slug,
Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision 2708282985faa68002d3d9051b403c0e12b11dd9)
+++ src/routes/posts.js	(revision d9ad6c564eccd0e4c23c3d8c2c8a32334f2443bc)
@@ -920,5 +920,5 @@
   const append = req.query.append === '1';
   const offset = Math.max(0, parseInt(req.query.offset, 10) || 0);
-  const page = site ? ActivityPubService.getMessages(site.slug, FEED_PAGE + 1, offset) : [];
+  const page = gateEmbeds(site, site ? ActivityPubService.getMessages(site.slug, FEED_PAGE + 1, offset) : []);
   const hasMore = page.length > FEED_PAGE;
   const items = page.slice(0, FEED_PAGE);
@@ -1084,4 +1084,20 @@
 }
 
+/**
+ * FEP-633c §5.3-style gated feature: may this account see previews of links
+ * that point OUTSIDE the fediverse? For a ward that is the guardians' call.
+ *
+ * Applied at SERVE time on every surface, the way the app's inbox read already
+ * does it (routes/activitypub.js): a card the client merely hides has still
+ * been delivered.
+ */
+function gateEmbeds(site, rows) {
+  if (!site || !rows.length) return rows;
+  let isWard = false;
+  try { isWard = Guardianship.listGuardians(site.slug).length > 0; } catch { /* no relations yet */ }
+  if (Guardianship.externalEmbedsAllowed(site.external_embeds, isWard)) return rows;
+  return rows.map((r) => (r && r.embed_json ? { ...r, embed_json: null } : r));
+}
+
 router.get('/news', requireSiteManager, (req, res) => {
   const site = res.locals.site;
@@ -1090,5 +1106,5 @@
   const cspOrigins = new Set();
   // Fetch one extra to know whether a "Load more" button belongs on this page.
-  const rows = site ? ActivityPubService.getTimeline(site.slug, FEED_PAGE + 1, offset) : [];
+  const rows = gateEmbeds(site, site ? ActivityPubService.getTimeline(site.slug, FEED_PAGE + 1, offset) : []);
   const hasMore = rows.length > FEED_PAGE;
   const timeline = rows.slice(0, FEED_PAGE).map((p) => {
