Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision d60c4987ede7cf7153f26e47e9178acea6dd38de)
+++ src/services/ActivityPubService.js	(revision ea3e72258d753919c01c4a4397857ebbe5890b4e)
@@ -29,5 +29,5 @@
 import Blocklist from './BlocklistService.js';
 import * as Guardianship from './guardianship/index.js';
-import { PUBLIC, AP_CONTEXT, safeUrl, actorId, noteId, guessMediaType, normalizeTags, tagParts } from './ap-core.js';
+import { PUBLIC, AP_CONTEXT, safeUrl, actorId, noteId, guessMediaType, normalizeTags, tagParts, hashtagTags, buildHashtagList } from './ap-core.js';
 // Doorgeven wat hier altijd vandaan kwam, zodat elke bestaande aanroep blijft werken.
 export { AP_CONTEXT, actorId, noteId, guessMediaType };
@@ -2960,15 +2960,4 @@
 
 // Extract the AP Hashtag tag objects from already-linked reply content.
-function hashtagTags(base, content) {
-  const tags = [], seen = new Set();
-  const re = /class="[^"]*\bhashtag\b[^"]*"[^>]*>#([\p{L}\p{M}\p{N}_]+)</giu;
-  let m;
-  while ((m = re.exec(content || ''))) {
-    const k = m[1].toLowerCase();
-    if (seen.has(k)) continue; seen.add(k);
-    tags.push({ type: 'Hashtag', href: `${base}/tag/${encodeURIComponent(k)}`, name: '#' + m[1] });
-  }
-  return tags;
-}
 
 // Normalise a post's tags field (array, JSON-string, or comma-string) to an array.
@@ -2980,16 +2969,6 @@
 // Merge a post's tags field + the #hashtags linked inline in its body into one deduped
 // Hashtag tag list (with hrefs to our /tag page).
-function buildHashtagList(base, tagsField, content) {
-  const out = [], seen = new Set();
-  for (const t of normalizeTags(tagsField)) {
-    const p = tagParts(t); if (!p || seen.has(p.slug)) continue; seen.add(p.slug);
-    out.push({ type: 'Hashtag', href: `${base}/tag/${encodeURIComponent(p.slug)}`, name: '#' + p.label });
-  }
-  for (const h of hashtagTags(base, content)) {
-    const k = h.name.slice(1).toLowerCase(); if (seen.has(k)) continue; seen.add(k);
-    out.push(h);
-  }
-  return out;
-}
+// hashtagTags en buildHashtagList staan sinds shaer-38y in ap-core: music/
+// heeft dezelfde lijst nodig en mag hier niet uit importeren.
 
 // Extract Mention tag objects from already-linked content (class="u-url mention").
Index: src/services/ap-core.js
===================================================================
--- src/services/ap-core.js	(revision d60c4987ede7cf7153f26e47e9178acea6dd38de)
+++ src/services/ap-core.js	(revision ea3e72258d753919c01c4a4397857ebbe5890b4e)
@@ -124,2 +124,42 @@
   return { label, slug };
 }
+
+/**
+ * De #hashtags die in het LIJF van een post gelinkt staan, zoals ze GESCHREVEN
+ * zijn. De slug in de href is kleingeschreven -- dat is een adres -- maar de
+ * naam niet: #DoenweNiet blijft #DoenweNiet.
+ */
+export function hashtagTags(base, content) {
+  const tags = [], seen = new Set();
+  const re = /class="[^"]*\bhashtag\b[^"]*"[^>]*>#([\p{L}\p{M}\p{N}_]+)</giu;
+  let m;
+  while ((m = re.exec(content || ''))) {
+    const k = m[1].toLowerCase();
+    if (seen.has(k)) continue; seen.add(k);
+    tags.push({ type: 'Hashtag', href: `${base}/tag/${encodeURIComponent(k)}`, name: '#' + m[1] });
+  }
+  return tags;
+}
+
+/**
+ * Het tagveld van een post en de #hashtags uit het lijf, samen en ontdubbeld.
+ *
+ * HET LIJF GAAT VOOR (Robin, 9-8): staat een tag allebei, dan wint de vorm
+ * zoals hij GESCHREVEN is. Het tagveld gaat door tagParts, en die maakt van
+ * "Doen we Niet" het CamelCase #DoenWeNiet -- nodig, want een hashtag mag geen
+ * spaties bevatten. Maar als iemand in zijn tekst #DoenweNiet heeft getypt is
+ * dat geen benadering meer maar de tag zelf, en dan hoort die te staan zoals
+ * hij er staat. Eerder won het veld, en verdween de geschreven vorm.
+ */
+export function buildHashtagList(base, tagsField, content) {
+  const out = [], seen = new Set();
+  for (const h of hashtagTags(base, content)) {
+    const k = h.name.slice(1).toLowerCase(); if (seen.has(k)) continue; seen.add(k);
+    out.push(h);
+  }
+  for (const t of normalizeTags(tagsField)) {
+    const p = tagParts(t); if (!p || seen.has(p.slug)) continue; seen.add(p.slug);
+    out.push({ type: 'Hashtag', href: `${base}/tag/${encodeURIComponent(p.slug)}`, name: '#' + p.label });
+  }
+  return out;
+}
Index: src/services/music/index.js
===================================================================
--- src/services/music/index.js	(revision d60c4987ede7cf7153f26e47e9178acea6dd38de)
+++ src/services/music/index.js	(revision ea3e72258d753919c01c4a4397857ebbe5890b4e)
@@ -14,5 +14,5 @@
 
 import db from '../../config/database.js';
-import { AP_CONTEXT, PUBLIC, actorId, noteId, safeUrl, guessMediaType, normalizeTags, tagParts } from '../ap-core.js';
+import { AP_CONTEXT, PUBLIC, actorId, noteId, safeUrl, guessMediaType, buildHashtagList } from '../ap-core.js';
 import { afleidenUitInsluitingen, ingeslotenPlaylists } from '../../assets/js/shared/post-music-type.js';
 
@@ -377,5 +377,8 @@
   }
 
-  const tags = hashtagsVanPost(base, post.tags);
+  // Dezelfde lijst als de Note: het tagveld EN de hashtags uit het lijf, waarbij
+  // de geschreven vorm voorgaat. Een eigen lijst hier zou de tags uit de tekst
+  // missen en de rest anders spellen dan dezelfde post elders doet.
+  const tags = buildHashtagList(base, post.tags, post.content);
   if (tags.length) obj.tag = tags;
 
@@ -386,22 +389,4 @@
   obj.context = noteId(base, post.id);
   return obj;
-}
-
-/**
- * De tags van een post als AS2 Hashtags -- zelfde vorm als buildHashtagList.
- *
- * Met normalizeTags en niet met een eigen split: het veld staat als JSON-array
- * in de database, en op komma's splitsen leverde live `#["Doen we Niet"` op.
- * Dat is het soort fout dat niet omvalt maar onzin uitlevert.
- */
-function hashtagsVanPost(base, tagsField) {
-  const uit = [], gezien = new Set();
-  for (const t of normalizeTags(tagsField)) {
-    const p = tagParts(t);
-    if (!p || gezien.has(p.slug)) continue;
-    gezien.add(p.slug);
-    uit.push({ type: 'Hashtag', href: `${base}/tag/${encodeURIComponent(p.slug)}`, name: '#' + p.label });
-  }
-  return uit;
 }
 
Index: test/music-uitgave.test.js
===================================================================
--- test/music-uitgave.test.js	(revision d60c4987ede7cf7153f26e47e9178acea6dd38de)
+++ test/music-uitgave.test.js	(revision ea3e72258d753919c01c4a4397857ebbe5890b4e)
@@ -74,6 +74,6 @@
   assert.equal(col.image.type, 'Image');
   assert.equal(col.image.url, `${BASE}/media/hoes.jpg`);
-  // Het tagveld is JSON in de database, en een tag van twee woorden wordt
-  // CamelCase -- zelfde regel als elders, want het is nu dezelfde parser.
+  // Het tagveld is JSON in de database. Een tag van twee woorden wordt
+  // CamelCase, want een hashtag mag geen spaties bevatten.
   assert.deepEqual(col.tag.map((t) => t.name), ['#lofi', '#ZolderTapes']);
   assert.equal(col.tag[1].href, `${BASE}/tag/zoldertapes`);
@@ -196,2 +196,19 @@
   assert.equal(M.buildPostTrackCollection(BASE, site, p).content, 'Opgenomen in de schuur.');
 });
+
+test('een hashtag uit het LIJF staat zoals hij geschreven is', () => {
+  // Robins punt (9-8): #DoenweNiet in de tekst hoort #DoenweNiet te blijven.
+  // Eerder won de vorm uit het tagveld -- tagParts maakt daar #DoenWeNiet van --
+  // en verdween de geschreven vorm als duplicaat.
+  const p = maakPost({
+    id: 'p-schrijf', slug: 'zoals-geschreven', titel: 'Zoals geschreven',
+    content: '<p>Zie <a class="hashtag" href="/tag/doenweniet">#DoenweNiet</a></p>[[track:t-een]]',
+    tags: '["Doen we Niet","lofi"]',
+  });
+  const col = M.buildPostTrackCollection(BASE, site, p);
+  const namen = col.tag.map((t) => t.name);
+  assert.ok(namen.includes('#DoenweNiet'), 'de geschreven vorm wint');
+  assert.ok(!namen.includes('#DoenWeNiet'), 'en de genormaliseerde staat er niet naast');
+  assert.ok(namen.includes('#lofi'), 'tags die alleen in het veld staan blijven');
+  assert.equal(col.tag[0].href, `${BASE}/tag/doenweniet`, 'de slug blijft wel klein: dat is een adres');
+});
