Index: src/assets/js/mod/playlist-editor.js
===================================================================
--- src/assets/js/mod/playlist-editor.js	(revision 21257e41df74e4553f4fb556b7e9fa030bffb2a3)
+++ src/assets/js/mod/playlist-editor.js	(revision d8f7d9c1d13b0b0315fce7adff55d967042095a0)
@@ -63,5 +63,5 @@
 
     // Existing playlist data when editing
-    let initial = { title: '', artist: '', year: '', cover: '', kind: 'album', track_ids: [] };
+    let initial = { title: '', artist: '', year: '', cover: '', kind: 'album', release_date: '', mb_release_id: '', track_ids: [] };
     if (isEdit && opts.id) {
       try {
@@ -102,4 +102,14 @@
                     <option value="playlist" ${initial.kind === 'playlist' ? 'selected' : ''}>📃 Playlist (track-covers)</option>
                   </select>
+                </label>
+                <label class="pl-field pl-uitgave">
+                  <span>Uitgavedatum</span>
+                  <input type="date" id="pli-release-date" value="${esc(initial.release_date || '')}">
+                </label>
+                <label class="pl-field pl-uitgave">
+                  <span>MusicBrainz release-id</span>
+                  <input type="text" id="pli-mb-release" maxlength="36" spellcheck="false"
+                         placeholder="00000000-0000-0000-0000-000000000000"
+                         value="${esc(initial.mb_release_id || '')}">
                 </label>
                 <div class="pl-field pl-field-full">
@@ -157,5 +167,18 @@
     const yearEl = $('#pli-year');
     const kindEl = $('#pli-kind');
+    const releaseEl = $('#pli-release-date');
+    const mbReleaseEl = $('#pli-mb-release');
     const coverEl = $('#pli-cover');
+
+    // Uitgavedatum en release-id horen bij een ALBUM, niet bij een
+    // afspeellijst -- dat is wat de keuze album/playlist betekent. Ze
+    // verdwijnen dus als je omschakelt, en de server maakt ze dan ook leeg;
+    // dit scherm is de uitleg, niet de bewaking.
+    const toonUitgave = () => {
+      const album = kindEl.value !== 'playlist';
+      for (const el of backdrop.querySelectorAll('.pl-uitgave')) el.hidden = !album;
+    };
+    kindEl.addEventListener('change', toonUitgave);
+    toonUitgave();
     const coverThumb = $('#pli-cover-thumb');
     const saveBtn = $('#pli-save');
@@ -376,4 +399,6 @@
         cover:  coverEl.value.trim(),
         kind:   kindEl.value === 'playlist' ? 'playlist' : 'album',
+        release_date:  releaseEl.value.trim(),
+        mb_release_id: mbReleaseEl.value.trim(),
         tracks: selected.slice(),
       };
Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision 21257e41df74e4553f4fb556b7e9fa030bffb2a3)
+++ src/config/database.js	(revision d8f7d9c1d13b0b0315fce7adff55d967042095a0)
@@ -140,4 +140,18 @@
   ensureColumn('sites', 'mb_artist_id', 'TEXT');
   ensureColumn('sites', 'mb_artist_name', 'TEXT');
+  // Een UITGAVE heeft twee dingen die een afspeellijst niet heeft (shaer-756s).
+  //
+  // release_date en niet `year`: die kolom bestaat al en blijft, maar hun
+  // AlbumSerializer leest `released` als een DateField. Een jaartal als
+  // 2024-01-01 versturen is een dag verzinnen, en dat is precies wat we bij
+  // artiesten en albums niet doen. Volledige datum of niets.
+  //
+  // mb_release_id is de tegenhanger van sites.mb_artist_id: dezelfde soort
+  // verwijzing naar MusicBrainz, een niveau lager.
+  //
+  // Ze horen ALLEEN bij kind='album'. PlaylistService dwingt dat af bij het
+  // opslaan -- zie daar waarom dat niet alleen in het scherm mag zitten.
+  ensureColumn('playlists', 'release_date', 'TEXT');
+  ensureColumn('playlists', 'mb_release_id', 'TEXT');
   ensureColumn('audio_tracks', 'cover_url', 'TEXT');
   ensureColumn('audio_tracks', 'album', 'TEXT');
Index: src/services/PlaylistService.js
===================================================================
--- src/services/PlaylistService.js	(revision 21257e41df74e4553f4fb556b7e9fa030bffb2a3)
+++ src/services/PlaylistService.js	(revision d8f7d9c1d13b0b0315fce7adff55d967042095a0)
@@ -15,4 +15,31 @@
 import { v4 as uuid } from 'uuid';
 
+/**
+ * Een volledige datum of niets (shaer-756s).
+ *
+ * STRIKT, en dat is de hele functie. `year` bestaat al en blijft; dit veld
+ * bestaat juist omdat een jaartal geen uitgavedatum is. Zou hij "2024"
+ * doorlaten en er 2024-01-01 van maken, dan stond er straks een dag op de
+ * federatie die niemand ooit heeft ingevoerd -- en dan hadden we het veld net
+ * zo goed niet kunnen toevoegen.
+ *
+ * Ook 2024-02-31 valt af: dat is geen strengheid om de strengheid, Date rolt
+ * hem stilletjes door naar 2 maart en dan slaan we iets anders op dan er
+ * ingetypt is.
+ */
+function normDatum(v) {
+  const s = String(v == null ? '' : v).trim();
+  if (!s) return null;
+  if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return null;
+  const d = new Date(`${s}T00:00:00Z`);
+  return Number.isNaN(d.getTime()) || d.toISOString().slice(0, 10) !== s ? null : s;
+}
+
+/** Een MusicBrainz-id of niets. Zelfde vorm als sites.mb_artist_id. */
+function normMbid(v) {
+  const s = String(v == null ? '' : v).trim().toLowerCase();
+  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(s) ? s : null;
+}
+
 class PlaylistService {
 
@@ -52,4 +79,5 @@
     const rows = db.prepare(`
       SELECT p.id, p.title, p.artist, p.year, p.cover_url, p.kind,
+             p.release_date, p.mb_release_id,
              p.created_at, p.updated_at,
              (SELECT COUNT(*) FROM playlist_tracks WHERE playlist_id = p.id) AS track_count
@@ -65,4 +93,6 @@
       cover: r.cover_url || '',
       kind: r.kind || 'album',
+      release_date: r.release_date || '',
+      mb_release_id: r.mb_release_id || '',
       track_count: r.track_count,
       created_at: r.created_at,
@@ -86,5 +116,5 @@
     if (!id) return null;
     const p = db.prepare(`
-      SELECT id, title, artist, year, cover_url, kind, created_at, updated_at
+      SELECT id, title, artist, year, cover_url, kind, release_date, mb_release_id, created_at, updated_at
       FROM playlists WHERE site_id = ? AND id = ?
     `).get(siteId, id);
@@ -125,4 +155,9 @@
       cover: p.cover_url || fallbackCover,
       kind: (p.kind === 'playlist') ? 'playlist' : 'album',
+      // Leeg als het een afspeellijst is -- de opslag houdt ze daar al leeg,
+      // maar dit is de plek waar de editor leest en die mag niet afhangen van
+      // wat er toevallig in de kolom stond.
+      release_date: p.kind === 'playlist' ? '' : (p.release_date || ''),
+      mb_release_id: p.kind === 'playlist' ? '' : (p.mb_release_id || ''),
       tracks: mappedTracks,
     };
@@ -141,8 +176,18 @@
     const kind = data.kind === 'playlist' ? 'playlist' : 'album';
 
+    // Alleen een UITGAVE draagt deze twee. Een afspeellijst heeft geen
+    // uitgavedatum en geen release-id, en dat onderscheid is precies wat de
+    // keuze album/playlist betekent (shaer-cyg). Het afdwingen gebeurt HIER en
+    // niet alleen in het scherm: een scherm kun je omzeilen -- de API ligt open
+    // voor de post-editor -- en dan staat er stille rommel op een mixtape die
+    // later als Album de deur uit gaat.
+    const uitgave = kind === 'album';
+    const releaseDate = uitgave ? normDatum(data.release_date) : null;
+    const mbRelease = uitgave ? normMbid(data.mb_release_id) : null;
+
     const tx = db.transaction(() => {
       db.prepare(`
-        INSERT INTO playlists (id, site_id, title, artist, year, cover_url, kind, created_at, updated_at)
-        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+        INSERT INTO playlists (id, site_id, title, artist, year, cover_url, kind, release_date, mb_release_id, created_at, updated_at)
+        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
       `).run(
         id, siteId, title,
@@ -150,5 +195,5 @@
         Number.isFinite(+data.year) && +data.year > 0 ? +data.year : null,
         String(data.cover || '').trim() || null,
-        kind, now, now,
+        kind, releaseDate, mbRelease, now, now,
       );
       this._writeTracks(id, siteId, data.tracks);
@@ -171,5 +216,5 @@
     if (!id) return false;
     const existing = db.prepare(
-      'SELECT id FROM playlists WHERE site_id = ? AND id = ?'
+      'SELECT id, kind FROM playlists WHERE site_id = ? AND id = ?'
     ).get(siteId, id);
     if (!existing) return false;
@@ -177,4 +222,9 @@
     const fields = [];
     const values = [];
+    // Wat wordt het NA deze wijziging? `kind` hoeft niet in data te staan, dus
+    // val terug op wat er ligt.
+    const nieuwKind = Object.prototype.hasOwnProperty.call(data, 'kind')
+      ? (data.kind === 'playlist' ? 'playlist' : 'album')
+      : (existing.kind || 'album');
     if (Object.prototype.hasOwnProperty.call(data, 'title')) {
       const v = String(data.title || '').trim();
@@ -193,5 +243,21 @@
     }
     if (Object.prototype.hasOwnProperty.call(data, 'kind')) {
-      fields.push('kind = ?'); values.push(data.kind === 'playlist' ? 'playlist' : 'album');
+      fields.push('kind = ?'); values.push(nieuwKind);
+    }
+    // De uitgavevelden. Wordt dit een afspeellijst, dan gaan ze ALTIJD leeg --
+    // ook als de aanroeper er niets over zei. Anders houdt een album dat je tot
+    // mixtape ombouwt zijn uitgavedatum en zijn release-id, en die duiken dan
+    // weer op zodra iemand hem terugzet. Een veld dat niet meer mag bestaan
+    // hoort weg te zijn, niet te wachten.
+    if (nieuwKind !== 'album') {
+      fields.push('release_date = ?'); values.push(null);
+      fields.push('mb_release_id = ?'); values.push(null);
+    } else {
+      if (Object.prototype.hasOwnProperty.call(data, 'release_date')) {
+        fields.push('release_date = ?'); values.push(normDatum(data.release_date));
+      }
+      if (Object.prototype.hasOwnProperty.call(data, 'mb_release_id')) {
+        fields.push('mb_release_id = ?'); values.push(normMbid(data.mb_release_id));
+      }
     }
     fields.push('updated_at = ?'); values.push(new Date().toISOString());
Index: src/views/partials/playlist-editor.ejs
===================================================================
--- src/views/partials/playlist-editor.ejs	(revision 21257e41df74e4553f4fb556b7e9fa030bffb2a3)
+++ src/views/partials/playlist-editor.ejs	(revision d8f7d9c1d13b0b0315fce7adff55d967042095a0)
@@ -99,4 +99,8 @@
 }
 .pl-field { display: flex; flex-direction: column; gap: .25rem; }
+/* `display: flex` wint van het hidden-attribuut, dus zonder deze regel doet
+   [hidden] op een .pl-field NIETS -- het veld blijft gewoon staan. De
+   uitgavevelden schakelen daarop (album versus afspeellijst). */
+.pl-field[hidden] { display: none; }
 .pl-field-full { grid-column: 1 / -1; }
 .pl-field > span {
Index: test/playlist-uitgave.test.js
===================================================================
--- test/playlist-uitgave.test.js	(revision d8f7d9c1d13b0b0315fce7adff55d967042095a0)
+++ test/playlist-uitgave.test.js	(revision d8f7d9c1d13b0b0315fce7adff55d967042095a0)
@@ -0,0 +1,101 @@
+// Uitgavevelden op een album-playlist (shaer-756s, stap 1).
+//
+// Twee kolommen die ALLEEN bij kind='album' horen: een volledige
+// uitgavedatum en een MusicBrainz release-id. Ze bestaan omdat hun
+// AlbumSerializer `released` als DateField leest en wij alleen `year` hadden --
+// en een jaartal als 2024-01-01 versturen is een dag verzinnen.
+//
+// Wat hier vooral getest wordt is wat er NIET binnenkomt. Het scherm verbergt
+// de velden bij een afspeellijst, maar de API ligt open (de post-editor gebruikt
+// hem ook), en een scherm is geen bewaking.
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+
+process.env.DATABASE_PATH = ':memory:';
+
+const dbMod = await import('../src/config/database.js');
+const db = dbMod.default;
+dbMod.initializeDatabase();
+const { default: PlaylistService } = await import('../src/services/PlaylistService.js');
+
+db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
+  .run('u1', 'u1', 'u1@t', 'x', 'god');
+db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)')
+  .run('s1', 'band', 'De Band', 'u1');
+
+const rij = (id) => db.prepare('SELECT * FROM playlists WHERE id = ?').get(id);
+
+test('een album bewaart een volledige datum en een release-id', () => {
+  const id = PlaylistService.create('s1', {
+    title: 'De Plaat', kind: 'album',
+    release_date: '2024-03-15', mb_release_id: '5441C29D-3602-4898-B1A1-B77FA23B8E50',
+  });
+  const r = rij(id);
+  assert.equal(r.release_date, '2024-03-15');
+  assert.equal(r.mb_release_id, '5441c29d-3602-4898-b1a1-b77fa23b8e50', 'mbid gaat naar kleine letters');
+});
+
+test('een halve datum wordt NIET aangevuld', () => {
+  // Dit is de hele reden dat het veld bestaat naast `year`. Zou hij hier
+  // 2024-01-01 van maken, dan stond er een dag op de federatie die niemand
+  // heeft ingevoerd.
+  for (const slecht of ['2024', '2024-03', '15-03-2024', 'maart 2024', '']) {
+    const id = PlaylistService.create('s1', { title: `T${slecht || 'leeg'}`, kind: 'album', release_date: slecht });
+    assert.equal(rij(id).release_date, null, `"${slecht}" had geweigerd moeten worden`);
+  }
+});
+
+test('een bestaande maar onmogelijke datum wordt geweigerd', () => {
+  // Date rolt 2024-02-31 stilletjes door naar 2 maart; dan sla je iets anders
+  // op dan er ingetypt is.
+  const id = PlaylistService.create('s1', { title: 'Onmogelijk', kind: 'album', release_date: '2024-02-31' });
+  assert.equal(rij(id).release_date, null);
+});
+
+test('een onzin-release-id wordt geweigerd', () => {
+  const id = PlaylistService.create('s1', { title: 'Onzin', kind: 'album', mb_release_id: 'niet-een-mbid' });
+  assert.equal(rij(id).mb_release_id, null);
+});
+
+test('een AFSPEELLIJST krijgt ze niet, ook niet als je ze meestuurt', () => {
+  const id = PlaylistService.create('s1', {
+    title: 'De Mix', kind: 'playlist',
+    release_date: '2024-03-15', mb_release_id: '5441c29d-3602-4898-b1a1-b77fa23b8e50',
+  });
+  const r = rij(id);
+  assert.equal(r.release_date, null);
+  assert.equal(r.mb_release_id, null);
+});
+
+test('omschakelen naar afspeellijst MAAKT ze leeg', () => {
+  // Anders houdt een album dat je tot mixtape ombouwt zijn uitgavedatum, en
+  // duikt die weer op zodra iemand hem terugzet.
+  const id = PlaylistService.create('s1', {
+    title: 'Wordt Mix', kind: 'album', release_date: '2024-03-15',
+    mb_release_id: '5441c29d-3602-4898-b1a1-b77fa23b8e50',
+  });
+  assert.equal(rij(id).release_date, '2024-03-15');
+  PlaylistService.update('s1', id, { kind: 'playlist' });   // zegt niets over de velden
+  const r = rij(id);
+  assert.equal(r.release_date, null, 'uitgavedatum bleef staan op een afspeellijst');
+  assert.equal(r.mb_release_id, null);
+});
+
+test('bijwerken zonder de velden te noemen laat ze met rust', () => {
+  const id = PlaylistService.create('s1', { title: 'Blijft', kind: 'album', release_date: '2024-03-15' });
+  PlaylistService.update('s1', id, { title: 'Blijft Ook' });
+  assert.equal(rij(id).release_date, '2024-03-15');
+});
+
+test('get en list geven de velden door, en houden ze leeg bij een afspeellijst', () => {
+  const a = PlaylistService.create('s1', { title: 'Uitgave', kind: 'album', release_date: '2024-03-15' });
+  assert.equal(PlaylistService.get('s1', a).release_date, '2024-03-15');
+  const inLijst = PlaylistService.list('s1').find((p) => p.id === a);
+  assert.equal(inLijst.release_date, '2024-03-15');
+
+  // Rechtstreeks in de kolom zetten op een afspeellijst -- zoals een oude rij
+  // eruit zou kunnen zien. Het leespad hoort dat niet door te geven.
+  const m = PlaylistService.create('s1', { title: 'Mix2', kind: 'playlist' });
+  db.prepare("UPDATE playlists SET release_date = '2024-01-01' WHERE id = ?").run(m);
+  assert.equal(PlaylistService.get('s1', m).release_date, '');
+});
