Index: src/routes/guardian.js
===================================================================
--- src/routes/guardian.js	(revision 47db4b55ef613863bde81368d52e3cce66e8969d)
+++ src/routes/guardian.js	(revision 54b027a0ddd850a81ee455880a44caa957a477cc)
@@ -262,5 +262,9 @@
       when_text: formatDateTime(p.published || p.created_at),
       cw: p.cw || null,
-      media: p.media_json ? JSON.parse(p.media_json) : [],
+      // Zelfde valkuil als in note-body.ejs: kapotte json gooit, maar geldige json
+      // van het verkeerde type niet. Zonder deze wacht neemt één vreemde note van
+      // een remote server het hele guardian-paneel mee, en dat is precies het
+      // scherm dat het moet doen als er iets aan de hand is.
+      media: (() => { try { const m = JSON.parse(p.media_json || '[]'); return Array.isArray(m) ? m : []; } catch { return []; } })(),
       // Een post van je ward hoort er hetzelfde uit te zien als in de Krant en
       // in Berichten: dezelfde partial, dus opmaak, media, quote-kaart en
Index: src/views/partials/note-body.ejs
===================================================================
--- src/views/partials/note-body.ejs	(revision 47db4b55ef613863bde81368d52e3cce66e8969d)
+++ src/views/partials/note-body.ejs	(revision 54b027a0ddd850a81ee455880a44caa957a477cc)
@@ -8,5 +8,12 @@
      Krant-only trimmings (the boost byline, polls, the audio player iframe, the
      action bar) stay in tl-item: they are about the feed, not about the post. */
+  // De catch vangt KAPOTTE json; hij vangt niet geldige json van het verkeerde
+  // TYPE. Drie posts van een remote server droegen media_json = "[]", dus een
+  // string met daarin [], en JSON.parse geeft dan netjes een string terug. Een
+  // string heeft geen .filter, en daarmee lag de hele Krant plat: één vreemde
+  // note nam de pagina mee. Wat er binnenkomt is niet van ons, dus de vorm moet
+  // hier afgedwongen worden en niet aangenomen.
   var _m = []; try { _m = JSON.parse(nb.media_json || '[]'); } catch (e) { _m = []; }
+  if (!Array.isArray(_m)) _m = [];
   var _imgs = _m.filter(function (m) { return m && m.url && (!m.type || /^image\//.test(m.type)); });
   var _vids = _m.filter(function (m) { return m && m.url && m.type && /^video\//.test(m.type); });
Index: test/note-body-media-shape.test.js
===================================================================
--- test/note-body-media-shape.test.js	(revision 54b027a0ddd850a81ee455880a44caa957a477cc)
+++ test/note-body-media-shape.test.js	(revision 54b027a0ddd850a81ee455880a44caa957a477cc)
@@ -0,0 +1,66 @@
+// Een vreemde note mag nooit de hele pagina meenemen.
+//
+// Drie posts van een remote server droegen media_json = "[]": een JSON-STRING met
+// daarin [], niet een array. De try/catch eromheen vangt kapotte json, maar niet
+// geldige json van het verkeerde type: JSON.parse('"[]"') geeft netjes de string
+// "[]" terug, en een string heeft geen .filter. Daarmee lag /news plat op beta.
+//
+// Wat er van een andere server binnenkomt is niet van ons. De vorm moet dus
+// afgedwongen worden op het moment van gebruik, niet aangenomen.
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import ejs from 'ejs';
+import path from 'path';
+
+const VIEWS = path.resolve('src/views');
+const PARTIAL = path.join(VIEWS, 'partials/note-body.ejs');
+
+// Alles wat note-body van een rij verwacht, met lege waarden. Per test overschrijven
+// we alleen media_json, want daar gaat het hier om.
+function rij(extra) {
+  return {
+    content: '<p>hoi</p>', media_json: null, emoji_json: null, quote_json: null,
+    embed_json: null, nsfw: 0, cw: null, poll_json: null, link_json: null,
+    ...extra,
+  };
+}
+
+async function render(nb) {
+  return ejs.renderFile(PARTIAL, {
+    nb,
+    noteQuote: () => null,
+    emojiHtml: (html) => String(html || ''),
+    thumb: (u) => String(u || ''),
+    t: (k) => k,
+    formatDate: (d) => String(d),
+    formatDateTime: (d) => String(d),
+  }, { async: true, views: [VIEWS] });
+}
+
+test('een media_json die een STRING is sloopt de pagina niet', async () => {
+  // Precies wat er op beta stond.
+  const html = await render(rij({ media_json: '"[]"' }));
+  assert.match(html, /hoi/, 'de tekst van de post hoort er gewoon te staan');
+});
+
+test('een media_json die een OBJECT is sloopt de pagina niet', async () => {
+  const html = await render(rij({ media_json: '{"url":"https://a.example/x.png"}' }));
+  assert.match(html, /hoi/);
+});
+
+test('een media_json die een GETAL is sloopt de pagina niet', async () => {
+  const html = await render(rij({ media_json: '42' }));
+  assert.match(html, /hoi/);
+});
+
+test('kapotte json blijft ook gewoon werken', async () => {
+  const html = await render(rij({ media_json: '{niet eens json' }));
+  assert.match(html, /hoi/);
+});
+
+test('en een NORMALE lijst wordt nog steeds getoond', async () => {
+  const html = await render(rij({
+    media_json: JSON.stringify([{ url: 'https://a.example/plaatje.png', type: 'image/png' }]),
+  }));
+  assert.match(html, /plaatje\.png/, 'de fix mag geen media wegpoetsen die wel klopt');
+});
