Index: src/services/music/index.js
===================================================================
--- src/services/music/index.js	(revision e667d26aec08a6380a8c2e1687a3f374682f383d)
+++ src/services/music/index.js	(revision c83206430014ce7861a00faf49d21b26ce8f3ad4)
@@ -336,5 +336,12 @@
 
 /**
- * Het type van een post afleiden uit de muziek die erin staat.
+ * Het type van een BESTAANDE post afleiden uit de muziek die erin staat.
+ *
+ * WAARVOOR DIT WEL EN NIET IS (Robins afbakening, 9-8). Nieuwe posts krijgen
+ * hun type uit de keuze: album of playlist wordt gekozen als de playlist wordt
+ * gemaakt, en de post neemt dat over. Deze functie is er voor wat er al staat --
+ * de posts met type=audio uit de tijd voor die keuze bestond. Daarmee is de
+ * vraag "wie wint, de keuze of de afleiding?" geen vraag meer: ze komen elkaar
+ * niet tegen. De afleiding draait eenmalig, de keuze draait daarna.
  *
  * DE REGEL GAAT OVER IDENTITEIT, NIET OVER TELLEN (Robins herformulering, 9-8):
@@ -357,19 +364,50 @@
  * letterlijk een album; hem als losse tracks tellen zou een albumpost tot
  * playlist maken. Telt hij straks anders, dan is dat hier een regel.
- */
-export function postMusicType(content) {
+ *
+ * WIE KIEST ALBUM OF PLAYLIST: dat gebeurt wanneer de PLAYLIST wordt gemaakt,
+ * en die keuze staat al in playlists.kind. Bij een enkele insluiting neemt de
+ * post dus die soort over -- een post om een album is een album, ook al heet de
+ * shortcode [[playlist:...]]. De soort van een playlist wordt hier dus
+ * OPGEZOCHT en niet afgeleid.
+ *
+ * EN ALS WE HET NIET WETEN: een gewone post met insluitingen die de post als
+ * context hebben. Dat is geen noodgreep maar de rustende toestand -- tracks
+ * wijzen met `context` toch al terug naar hun post, dus er gaat niets verloren
+ * als het label 'post' wordt.
+ *
+ * @param {string} content   de HTML/tekst van de post
+ * @param {string} siteId    nodig om playlists.kind te kunnen opzoeken
+ */
+export function postMusicType(content, siteId) {
   const c = String(content || '');
   const uniek = (re) => [...new Set([...c.matchAll(re)].map((m) => m[1].trim()))];
 
-  const playlists = uniek(/\[\[playlist:([A-Za-z0-9_-]+)\]\]/g);
+  // Dezelfde patronen als de renderer in AudioEmbedService: wat daar niet
+  // insluit, telt hier niet mee. Anders zou een shortcode die niets oplevert
+  // wel het type van de post kunnen bepalen.
+  const playlists = uniek(/\[\[playlist:([a-z0-9][a-z0-9-]*)\]\]/gi);
   const albums    = uniek(/\[\[album:([^\]]+)\]\]/g);
   const tracks    = uniek(/\[\[track:([A-Za-z0-9_-]+)\]\]/g);
 
+  if (!playlists.length && !albums.length && !tracks.length) return null;
+
+  // De soort van een playlist is een gegeven, geen afleiding.
+  let onbekend = [];
+  const uitPlaylists = playlists.map((id) => {
+    const kind = playlistKind(id, siteId);
+    if (!kind) { onbekend.push(id); return null; }
+    return { soort: kind, id };
+  }).filter(Boolean);
+
   const collecties = [
-    ...playlists.map((id) => ({ soort: 'playlist', id })),
+    ...uitPlaylists,
     ...albums.map((naam) => ({ soort: 'album', naam })),
   ];
 
-  if (!collecties.length && !tracks.length) return null;          // geen muziek
+  // Onbekende situatie -> gewone post. De insluitingen blijven staan en houden
+  // de post als context; alleen het label wordt niet verzonnen.
+  if (onbekend.length) {
+    return { type: 'post', collecties, tracks, bonus: [], onbekend, leentMetadata: false };
+  }
 
   if (!collecties.length) {
@@ -389,2 +427,16 @@
   return { type: 'post', collecties, tracks, bonus: [], leentMetadata: false };
 }
+
+/**
+ * De gekozen soort van een playlist: 'album' | 'playlist', of null als hij niet
+ * (op deze site) bestaat. Zelfde normalisatie als PlaylistService: alles wat
+ * geen 'playlist' zegt is een album.
+ */
+function playlistKind(id, siteId) {
+  if (!siteId) return null;
+  try {
+    const r = db.prepare('SELECT kind FROM playlists WHERE id = ? AND site_id = ?').get(id, siteId);
+    if (!r) return null;
+    return r.kind === 'playlist' ? 'playlist' : 'album';
+  } catch { return null; }
+}
Index: test/music-type.test.js
===================================================================
--- test/music-type.test.js	(revision e667d26aec08a6380a8c2e1687a3f374682f383d)
+++ test/music-type.test.js	(revision c83206430014ce7861a00faf49d21b26ce8f3ad4)
@@ -1,9 +1,11 @@
-// Welk soort uitgave is een post? (shaer-cyg)
+// Welk soort uitgave is een BESTAANDE post? (shaer-cyg)
 //
-// De regel gaat over IDENTITEIT en niet over tellen: een post neemt het type
-// van zijn muziek over als hij precies EEN muzikale eenheid bevat. Zijn het er
-// meer, dan is de post een post die naar muziek verwijst en houden de
-// collecties hun eigen identiteit -- en vervalt de reden om de posttitel en
-// -cover te lenen.
+// Nieuwe posts krijgen hun type uit de keuze -- album of playlist wordt gekozen
+// als de playlist wordt gemaakt. Deze afleiding is er voor wat er al staat: de
+// type=audio posts van voor die keuze. De regel gaat over IDENTITEIT en niet
+// over tellen: een post neemt het type van zijn muziek over als hij precies EEN
+// muzikale eenheid bevat. Zijn het er meer, dan is de post een post die naar
+// muziek verwijst en houden de collecties hun eigen identiteit -- en vervalt de
+// reden om de posttitel en -cover te lenen.
 import { test } from 'node:test';
 import assert from 'node:assert/strict';
@@ -14,14 +16,33 @@
 const dbMod = await import('../src/config/database.js');
 dbMod.initializeDatabase();
+const db = dbMod.default;
 const { postMusicType } = await import('../src/services/music/index.js');
 
+const SITE = 'site-muziek';
+const ANDERE = 'site-anders';
+db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
+  .run('u1', 'robin', 'r@test', 'x', 'god');
+db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run(SITE, 'muziek', 'Muziek', 'u1');
+db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run(ANDERE, 'anders', 'Anders', 'u1');
+
+const maakPlaylist = (id, kind, siteId = SITE) =>
+  db.prepare('INSERT INTO playlists (id, site_id, title, kind) VALUES (?, ?, ?, ?)')
+    .run(id, siteId, id, kind);
+
+maakPlaylist('nachtlicht', 'playlist');
+maakPlaylist('de-plaat', 'album');
+maakPlaylist('tweede', 'playlist');
+maakPlaylist('elders', 'playlist', ANDERE);
+
+const type = (c) => postMusicType(c, SITE);
+
 test('geen muziek: geen type', () => {
-  assert.equal(postMusicType('<p>gewoon een bericht</p>'), null);
-  assert.equal(postMusicType(''), null);
-  assert.equal(postMusicType(null), null);
+  assert.equal(type('<p>gewoon een bericht</p>'), null);
+  assert.equal(type(''), null);
+  assert.equal(type(null), null);
 });
 
 test('losse tracks worden een playlist, en lenen de metadata van de post', () => {
-  const r = postMusicType('Twee nieuwe: [[track:a]] en [[track:b]]');
+  const r = type('Twee nieuwe: [[track:a]] en [[track:b]]');
   assert.equal(r.type, 'playlist');
   assert.deepEqual(r.tracks, ['a', 'b']);
@@ -32,10 +53,17 @@
   // Naar buiten toe is er dan altijd een collectie om naar te wijzen. Dat
   // "Playlist" boven een enkel nummer gek leest is een weergavekwestie.
-  assert.equal(postMusicType('[[track:a]]').type, 'playlist');
+  assert.equal(type('[[track:a]]').type, 'playlist');
 });
 
-test('een playlist blijft een playlist', () => {
-  const r = postMusicType('Nieuw: [[playlist:nachtlicht]]');
-  assert.equal(r.type, 'playlist');
+test('de post neemt de GEKOZEN soort van de playlist over', () => {
+  // De kern van Robins besluit: album of playlist is bij het maken van de
+  // playlist al gekozen. Hier wordt hij opgezocht, niet afgeleid.
+  assert.equal(type('Nieuw: [[playlist:nachtlicht]]').type, 'playlist');
+  assert.equal(type('Nieuw: [[playlist:de-plaat]]').type, 'album',
+    'een post om een album is een album, ook al heet de shortcode playlist');
+});
+
+test('de collectie draagt haar eigen soort mee', () => {
+  const r = type('Nieuw: [[playlist:nachtlicht]]');
   assert.deepEqual(r.collectie, { soort: 'playlist', id: 'nachtlicht' });
   assert.equal(r.leentMetadata, true);
@@ -43,7 +71,8 @@
 
 test('een album-insluiting is ook een collectie, en telt als album', () => {
-  // Robins regel noemde alleen playlists; [[album:naam]] groepeert net zo goed
-  // tracks. Hem als losse tracks tellen zou een albumpost tot playlist maken.
-  const r = postMusicType('Luister [[album:De Plaat]]');
+  // [[album:naam]] groepeert tracks op hun albumveld -- geen playlist-rij, dus
+  // geen gekozen soort. Hem als losse tracks tellen zou een albumpost tot
+  // playlist maken.
+  const r = type('Luister [[album:De Plaat]]');
   assert.equal(r.type, 'album');
   assert.deepEqual(r.collectie, { soort: 'album', naam: 'De Plaat' });
@@ -51,6 +80,6 @@
 
 test('een collectie MET losse tracks is een album, en die losse zijn bonus', () => {
-  const r = postMusicType('[[playlist:nachtlicht]] plus [[track:x]] en [[track:y]]');
-  assert.equal(r.type, 'album');
+  const r = type('[[playlist:nachtlicht]] plus [[track:x]] en [[track:y]]');
+  assert.equal(r.type, 'album', 'ook al is de playlist zelf een playlist');
   assert.equal(r.collectie.id, 'nachtlicht');
   assert.deepEqual(r.bonus, ['x', 'y'], 'geen rommelrestje maar bonus-tracks');
@@ -60,5 +89,5 @@
 
 test('TWEE collecties: de post blijft een post en leent niets', () => {
-  const r = postMusicType('[[playlist:een]] en ook [[playlist:twee]]');
+  const r = type('[[playlist:nachtlicht]] en ook [[playlist:tweede]]');
   assert.equal(r.type, 'post');
   assert.equal(r.collecties.length, 2);
@@ -68,5 +97,5 @@
 
 test('twee collecties van verschillende soort tellen ook als twee', () => {
-  const r = postMusicType('[[playlist:een]] naast [[album:De Plaat]]');
+  const r = type('[[playlist:nachtlicht]] naast [[album:De Plaat]]');
   assert.equal(r.type, 'post');
   assert.equal(r.collecties.length, 2);
@@ -76,10 +105,29 @@
   // Anders maakt een herhaalde shortcode van een albumpost stilletjes een
   // gewone post.
-  const r = postMusicType('[[playlist:een]] ... en nog eens [[playlist:een]]');
+  const r = type('[[playlist:nachtlicht]] ... en nog eens [[playlist:nachtlicht]]');
   assert.equal(r.type, 'playlist');
-  assert.equal(r.collectie.id, 'een');
+  assert.equal(r.collectie.id, 'nachtlicht');
 });
 
 test('dezelfde track twee keer telt ook maar een keer', () => {
-  assert.deepEqual(postMusicType('[[track:a]] [[track:a]] [[track:b]]').tracks, ['a', 'b']);
+  assert.deepEqual(type('[[track:a]] [[track:a]] [[track:b]]').tracks, ['a', 'b']);
 });
+
+test('onbekende playlist: gewone post, niets verzonnen', () => {
+  // Robins laatste optie voor een onbekende situatie. De insluiting blijft
+  // staan en houdt de post als context; alleen het label wordt niet geraden.
+  const r = type('[[playlist:bestaat-niet]]');
+  assert.equal(r.type, 'post');
+  assert.deepEqual(r.onbekend, ['bestaat-niet']);
+  assert.equal(r.leentMetadata, false);
+});
+
+test('een playlist van een ANDERE site telt niet als de onze', () => {
+  const r = type('[[playlist:elders]]');
+  assert.equal(r.type, 'post');
+  assert.deepEqual(r.onbekend, ['elders']);
+});
+
+test('zonder site is er niets op te zoeken, dus geen type verzinnen', () => {
+  assert.equal(postMusicType('[[playlist:nachtlicht]]').type, 'post');
+});
