Index: src/services/music/index.js
===================================================================
--- src/services/music/index.js	(revision 7090fd2a91b6ac5a5468359f0b0064888f4107f7)
+++ src/services/music/index.js	(revision 9026de5942183fd216c7f6d37ead649fb6545db3)
@@ -485,11 +485,24 @@
   if (!pl) return null;
   const abs = (u) => !u ? null : (/^https?:/i.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`);
-  // GEEN epoch als terugval. `published` is bij hen verplicht, maar 1970 is een
-  // ANTWOORD en geen ontbrekend veld -- en dat is erger: een lezer kan een gat
-  // opmerken, een leugen niet. playlists.created_at heeft een default, dus als
-  // hij hier ontbreekt is er een leespad dat de kolom laat vallen. Dat willen we
-  // zien, niet maskeren. (Zo kwam op 16-8 de route boven water die id, title,
-  // artist, year, cover_url en kind selecteerde en de rest niet.)
-  const wanneer = pl.created_at ? new Date(pl.created_at).toISOString() : null;
+  // WANNEER IS DEZE PLAAT GEPUBLICEERD. De post die hem uitbrengt gaat voor, en
+  // niet als noodgreep maar omdat hij het beter weet: playlists.created_at is
+  // het moment waarop de RIJ is aangemaakt, en dat kan weken eerder zijn terwijl
+  // je nog aan het samenstellen was. AS2 `published` vraagt wanneer het object
+  // openbaar werd, en dat is de post.
+  //
+  // GEEN epoch als laatste terugval. `published` is bij hen verplicht, maar 1970
+  // is een ANTWOORD en geen ontbrekend veld -- en dat is erger: een lezer kan een
+  // gat opmerken, een leugen niet. Zo kwam op 16-8 de route boven water die id,
+  // title, artist, year, cover_url en kind selecteerde en de rest niet.
+  //
+  // OOK VOOR `released`, en daar had ik het eerst mis (Robin, 16-8). Mijn
+  // bezwaar was: post je vandaag een plaat uit 2018, dan beweert dit dat hij
+  // vandaag uitkwam. Dat gebeurt ook -- maar bij de meeste Klonkt-sites IS de
+  // post het uitbrengen, en GEEN datum is slechter dan een datum die op het
+  // gewone geval klopt. Het handmatige veld is precies het gereedschap voor de
+  // uitzondering: bij een heruitgave vul je hem in en die wint.
+  const postDatum = (pl._post && pl._post.uit_wanneer) ? new Date(pl._post.uit_wanneer) : null;
+  const wanneer = postDatum ? postDatum.toISOString()
+    : (pl.created_at ? new Date(pl.created_at).toISOString() : null);
   // Dezelfde lening als in buildPlaylistCollection: de post die de plaat
   // uitbrengt geeft zijn titel, en de eigen titel blijft als alsoKnownAs staan.
@@ -504,7 +517,10 @@
   };
   if (titel !== pl.title) album.alsoKnownAs = pl.title;
-  // `released` alleen als er een ECHTE datum is. `year` vult hem niet aan: een
-  // jaartal is geen dag, en dat is de reden dat release_date bestaat.
+  // Het ingevulde veld wint altijd; anders de DAG waarop de post verscheen.
+  // `year` vult hem nog steeds niet aan, en dat is geen inconsequentie: een
+  // jaartal is geen dag, terwijl de postdatum een gebeurtenis is die werkelijk
+  // heeft plaatsgevonden. Het verschil is verzinnen versus afleiden.
   if (pl.release_date) album.released = pl.release_date;
+  else if (postDatum) album.released = postDatum.toISOString().slice(0, 10);
   if (pl.mb_release_id) album.musicbrainzId = pl.mb_release_id;
   const hoes = abs(pl.cover_url || null);
@@ -522,4 +538,9 @@
   const hostPosts = site.id ? trackHostPosts(site.id) : null;
   const albums = site.id ? trackAlbums(site.id) : null;
+  // EEN keer opzoeken en twee keer gebruiken: het Album leent er zijn datums en
+  // titel van, leenVanPost onderaan zijn tekst en tags. Twee losse aanroepen
+  // zouden niet alleen dubbel werk zijn maar ook uiteen kunnen lopen -- en dan
+  // staat er weer iets anders op het ingesloten object dan op zijn eigen URI.
+  const post = site.id ? uitgavePost(site.id, playlist.id) : null;
   const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { coverFallback: playlist.cover_url || null, hostPosts, albums }));
   const out = pagedCollection(`${actorId(base, site.slug)}/playlists/${playlist.id}`, items, {
@@ -547,10 +568,10 @@
   // staat toch al ingesloten op de track.
   if ((playlist.kind || 'album') === 'album') {
-    const album = buildAlbumObject(base, site, playlist);
+    const album = buildAlbumObject(base, site, { ...playlist, _post: post });
     for (const veld of ['published', 'released', 'musicbrainzId', 'artist_credit', 'image']) {
       if (album[veld] !== undefined) out[veld] = album[veld];
     }
   }
-  return leenVanPost(base, site, out, uitgavePost(site.id, playlist.id));
+  return leenVanPost(base, site, out, post);
 }
 
@@ -577,5 +598,6 @@
   try {
     const rijen = db.prepare(`
-      SELECT id, slug, title, excerpt, content, cover_image_url, tags
+      SELECT id, slug, title, excerpt, content, cover_image_url, tags,
+             COALESCE(published_at, created_at) AS uit_wanneer
       FROM posts
       WHERE site_id = ? AND status = 'published'
Index: test/ap-fw-track.test.js
===================================================================
--- test/ap-fw-track.test.js	(revision 7090fd2a91b6ac5a5468359f0b0064888f4107f7)
+++ test/ap-fw-track.test.js	(revision 9026de5942183fd216c7f6d37ead649fb6545db3)
@@ -204,2 +204,66 @@
   assert.equal(col.artist_credit, undefined);
 });
+
+test('published EN released lenen de datum van de uitgavepost', () => {
+  // De post is het moment van uitbrengen. Geen datum is slechter dan een datum
+  // die op het gewone geval klopt; voor een heruitgave vul je het veld in.
+  db.prepare(`INSERT INTO posts (id, site_id, author_id, slug, title, content, status, published_at)
+              VALUES ('p-uit','s1','u1','de-plaat-uit','De Plaat Is Er','<p>[[playlist:de-plaat]]</p>','published','2026-05-04T12:00:00Z')`).run();
+  // De fixture zette hierboven al een release_date; die wint terecht, dus voor
+  // DEZE test moet hij weg. (Dat de eerste versie hier omviel is het bewijs dat
+  // de voorrang werkt.)
+  db.prepare("UPDATE playlists SET release_date = NULL WHERE id = 'de-plaat'").run();
+  const al = audio('t1').track.album;
+  assert.equal(al.published, '2026-05-04T12:00:00.000Z');
+  assert.equal(al.released, '2026-05-04', 'released is een DAG, geen tijdstip');
+});
+
+test('een ingevulde uitgavedatum wint van de postdatum', () => {
+  // Dit is het geval waarvoor het veld bestaat: oud werk dat je vandaag post.
+  db.prepare("UPDATE playlists SET release_date = '2018-09-01' WHERE id = 'de-plaat'").run();
+  const al = audio('t1').track.album;
+  assert.equal(al.released, '2018-09-01', 'de postdatum overschreef het ingevulde veld');
+  db.prepare("UPDATE playlists SET release_date = NULL WHERE id = 'de-plaat'").run();
+});
+
+test('geen post en geen veld: dan geen released', () => {
+  // Afwezig is afwezig -- we leiden af waar er iets af te leiden valt, en
+  // verzinnen niets waar dat niet zo is.
+  db.prepare("INSERT INTO playlists (id, site_id, title, kind) VALUES ('kaal','s1','Kale Plaat','album')").run();
+  db.prepare("INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES ('kaal','t9',1)").run();
+  const al = audio('t9').track.album;
+  assert.equal(al.name, 'Kale Plaat');
+  assert.equal(al.released, undefined);
+});
+
+test('een GEPLANDE post: de geplande dag telt, niet wanneer de worker draaide', () => {
+  // Robins vraag (16-8). Drie schakels moeten kloppen:
+  //
+  //   1. zolang de post 'scheduled' is vindt uitgavePost hem NIET (die filtert
+  //      op status='published'), dus de plaat heeft nog geen uitgavedatum --
+  //      hij is immers nog niet uit;
+  //   2. de Scheduler zet published_at op COALESCE(published_at, publish_at,
+  //      CURRENT_TIMESTAMP), dus op de GEPLANDE tijd;
+  //   3. wij lezen COALESCE(published_at, created_at) en komen daar dus op uit.
+  //
+  // Dat derde punt is waarom created_at hier nooit wint: bij een gepubliceerde
+  // post staat published_at altijd gevuld. En punt 2 is waarom een server die
+  // een uur plat lag geen uur te late uitgavedatum oplevert.
+  db.prepare("INSERT INTO playlists (id, site_id, title, kind) VALUES ('gepland','s1','Geplande Plaat','album')").run();
+  db.prepare("INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES ('gepland','t2',1)").run();
+  db.prepare(`INSERT INTO posts (id, site_id, author_id, slug, title, content, status, publish_at, created_at, published_at)
+              VALUES ('p-gepland','s1','u1','komt-nog','Komt Nog','<p>[[playlist:gepland]]</p>','scheduled',
+                      '2026-12-24T09:00:00Z','2026-06-01T08:00:00Z',NULL)`).run();
+
+  // Nog gepland: geen datum, want de plaat is nog niet uit.
+  assert.equal(audio('t2').track.album.released, undefined, 'een geplande post bracht al iets uit');
+
+  // De Scheduler doet zijn werk -- en draait expres LATER dan gepland.
+  db.prepare(`UPDATE posts SET status = 'published',
+              published_at = COALESCE(published_at, publish_at, CURRENT_TIMESTAMP) WHERE id = 'p-gepland'`).run();
+
+  const al = audio('t2').track.album;
+  assert.equal(al.released, '2026-12-24', 'niet de geplande dag');
+  assert.equal(al.published, '2026-12-24T09:00:00.000Z');
+  assert.notEqual(al.released, '2026-06-01', 'de creatiedatum lekte door');
+});
