Index: src/routes/feed.js
===================================================================
--- src/routes/feed.js	(revision 34e63f06ca6b67732e6929f4e9fc59c0017e608c)
+++ src/routes/feed.js	(revision 4d81de5db2cefacf9f554f61696388ca4fbb4a9d)
@@ -66,4 +66,22 @@
   const tracks = siteOpenTracks(site.id);
   const lastBuild = posts[0]?.published_at || new Date().toISOString();
+
+  // De itunes-velden waar een podcast-app een kanaal aan herkent. Funkwhale
+  // bouwde onze kanaalpagina langs de RSS-kant op en liet de categorie leeg,
+  // want die leest hij hier -- niet uit `category` op de AP-actor.
+  //
+  // De categorie volgt DEZELFDE regel als daar: alleen 'Music' als er ook
+  // werkelijk publieke muziek is. Een blog zonder open track is naar buiten
+  // toe geen muziekkanaal, en gated muziek telt niet mee -- afwezig is
+  // afwezig, ook in een categorie.
+  const abs = (u) => !u ? null : (/^https?:/i.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`);
+  const kanaalKunst = abs(site.profile_photo || site.og_image_default || null);
+  const kanaalTags = [
+    `    <itunes:author>${escapeXml(site.author || site.title || '')}</itunes:author>`,
+    site.description || site.tagline
+      ? `    <itunes:summary>${escapeXml(site.description || site.tagline)}</itunes:summary>` : null,
+    kanaalKunst ? `    <itunes:image href="${escapeXml(kanaalKunst)}" />` : null,
+    tracks.length ? '    <itunes:category text="Music" />' : null,
+  ].filter(Boolean).join('\n');
 
   const wanneer = (d) => { const t = Date.parse(d); return Number.isNaN(t) ? 0 : t; };
@@ -88,5 +106,7 @@
       <description>${escapeXml(t.artist || '')}</description>
       <enclosure url="${escapeXml(`${base}/audio/stream/${encodeURIComponent(fn)}`)}" length="${Number(t.size) || 0}" type="${escapeXml(t.mime_type || 'audio/mpeg')}" />${t.duration ? `
-      <itunes:duration>${Math.round(t.duration)}</itunes:duration>` : ''}
+      <itunes:duration>${Math.round(t.duration)}</itunes:duration>` : ''}${t.artist ? `
+      <itunes:author>${escapeXml(t.artist)}</itunes:author>` : ''}${abs(t.cover_url) ? `
+      <itunes:image href="${escapeXml(abs(t.cover_url))}" />` : ''}
     </item>` };
     }),
@@ -103,4 +123,5 @@
     <lastBuildDate>${new Date(lastBuild).toUTCString()}</lastBuildDate>
     <atom:link href="${escapeXml(base + '/feed.xml')}" rel="self" type="application/rss+xml" />
+${kanaalTags}
 ${items}
   </channel>
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 34e63f06ca6b67732e6929f4e9fc59c0017e608c)
+++ src/services/ActivityPubService.js	(revision 4d81de5db2cefacf9f554f61696388ca4fbb4a9d)
@@ -233,7 +233,13 @@
  * de kast, niet enable_audio_player -- die staat standaard aan en zegt niets.
  */
-function channelCategory(site) {
-  try {
-    return db.prepare('SELECT 1 FROM audio_tracks WHERE site_id = ? LIMIT 1').get(site.id) ? 'music' : null;
+export function channelCategory(site) {
+  try {
+    // ALLEEN opengezette tracks tellen. Eerst keek dit naar elke track, ook een
+    // gated -- en dan roept een site met uitsluitend afgeschermde muziek toch
+    // "hier is muziek" naar de hele fediverse. Dat botst met de regel die we
+    // overal aanhouden: een gesloten track is AFWEZIG, niet stilletjes
+    // aanwezig. Naar buiten toe is een kanaal zonder publieke muziek geen
+    // muziekkanaal.
+    return db.prepare('SELECT 1 FROM audio_tracks WHERE site_id = ? AND fedi_open = 1 LIMIT 1').get(site.id) ? 'music' : null;
   } catch { return null; }
 }
@@ -6039,5 +6045,5 @@
   AP_CONTEXT, getOrCreateKeys, apWants, sendAP, actorId, noteId, stripLeadingMentions,
   buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFollowing, buildFeatured,
-  channelUrls, timelineFields, guessMediaType,
+  channelUrls, channelCategory, timelineFields, guessMediaType,
   siteOpenTracks, openTrack, buildTrackAudio, buildTrackCollection, buildTrackCreate,
   buildPlaylistCollection, playlistOpenTracks, listPlaylistsAP, playlistLinkTags,
Index: test/channel-actor.test.js
===================================================================
--- test/channel-actor.test.js	(revision 34e63f06ca6b67732e6929f4e9fc59c0017e608c)
+++ test/channel-actor.test.js	(revision 4d81de5db2cefacf9f554f61696388ca4fbb4a9d)
@@ -36,8 +36,19 @@
 });
 
-test('met muziek is de category "music"', () => {
+test('een GATED track maakt er nog geen muziekkanaal van', () => {
+  // Aangescherpt: eerst telde elke track mee, ook een afgeschermde -- dan riep
+  // een site met uitsluitend gated muziek toch "hier is muziek" naar buiten.
+  // Afwezig is afwezig, ook in een categorie.
+  db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,1)')
+    .run('m0', 's1', 'dicht.mp3', 'audio/dicht.mp3', 'audio/mpeg');
+  db.prepare('INSERT INTO audio_tracks (id, site_id, title, media_id, fedi_open) VALUES (?,?,?,?,0)')
+    .run('t0', 's1', 'Achter de poort', 'm0');
+  assert.equal(AP.buildActor(BASE, site()).category, undefined);
+});
+
+test('met OPENGEZETTE muziek is de category "music"', () => {
   db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,1)')
     .run('m1', 's1', 'a.mp3', 'audio/a.mp3', 'audio/mpeg');
-  db.prepare('INSERT INTO audio_tracks (id, site_id, title, media_id) VALUES (?,?,?,?)')
+  db.prepare('INSERT INTO audio_tracks (id, site_id, title, media_id, fedi_open) VALUES (?,?,?,?,1)')
     .run('t1', 's1', 'Een nummer', 'm1');
   assert.equal(AP.buildActor(BASE, site()).category, 'music');
Index: test/feed-itunes.test.js
===================================================================
--- test/feed-itunes.test.js	(revision 4d81de5db2cefacf9f554f61696388ca4fbb4a9d)
+++ test/feed-itunes.test.js	(revision 4d81de5db2cefacf9f554f61696388ca4fbb4a9d)
@@ -0,0 +1,81 @@
+// De itunes-velden waaraan een podcast-app een kanaal herkent (shaer-0nh).
+//
+// Aanleiding: Funkwhale maakte van dev@dev.klonkt.com een kanaal en las de
+// rss_url uit onze actor, maar liet content_category leeg. Die leest hij hier,
+// in de feed -- niet uit `category` op de AP-actor.
+//
+// De categorie volgt DEZELFDE regel als op de actor: alleen Music als er ook
+// werkelijk publieke muziek is. Gated telt niet mee.
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+
+process.env.DATABASE_PATH = ':memory:';
+process.env.PUBLIC_BASE_URL = 'https://test.example';
+
+const dbMod = await import('../src/config/database.js');
+const db = dbMod.default;
+dbMod.initializeDatabase();
+const express = (await import('express')).default;
+const feed = (await import('../src/routes/feed.js')).default;
+const AP = (await import('../src/services/ActivityPubService.js')).default;
+
+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, tagline, author, profile_photo, owner_id, is_public, is_primary)
+            VALUES (?,?,?,?,?,?,?,1,1)`)
+  .run('s1', 'band', 'De Band', 'Wij maken lawaai', 'De Band zelf', '/media/avatar.webp', 'u1');
+db.prepare(`INSERT INTO posts (id, site_id, author_id, slug, title, excerpt, content, status, published_at)
+            VALUES (?,?,?,?,?,?,?,'published',?)`)
+  .run('p1', 's1', 'u1', 'bericht', 'Bericht', 'kort', '<p>x</p>', '2026-01-01T00:00:00Z');
+
+const app = express();
+app.use((req, res, next) => {
+  res.locals.site = db.prepare("SELECT * FROM sites WHERE id = 's1'").get();
+  res.locals.siteUrlBase = '';
+  next();
+});
+app.use(feed);
+const server = app.listen(0);
+await new Promise((r) => server.once('listening', r));
+const haal = async () => (await fetch(`http://127.0.0.1:${server.address().port}/feed.xml`)).text();
+
+const site = () => db.prepare("SELECT * FROM sites WHERE id = 's1'").get();
+
+test('zonder publieke muziek: wel auteur, samenvatting en plaat, GEEN categorie', async () => {
+  const xml = await haal();
+  assert.match(xml, /<itunes:author>De Band zelf<\/itunes:author>/);
+  assert.match(xml, /<itunes:summary>Wij maken lawaai<\/itunes:summary>/);
+  assert.match(xml, /<itunes:image href="[^"]*\/media\/avatar\.webp" \/>/, 'de plaat wordt absoluut gemaakt');
+  assert.ok(!xml.includes('<itunes:category'), 'een blog zonder open track is geen muziekkanaal');
+  assert.equal(AP.channelCategory(site()), null, 'en de actor zegt hetzelfde');
+});
+
+test('een GATED track maakt er nog geen muziekkanaal van', async () => {
+  db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,?)')
+    .run('m0', 's1', 'dicht.mp3', 'audio/dicht.mp3', 'audio/mpeg', 10);
+  db.prepare('INSERT INTO audio_tracks (id, site_id, title, media_id, fedi_open) VALUES (?,?,?,?,0)')
+    .run('t0', 's1', 'Achter de poort', 'm0');
+  const xml = await haal();
+  assert.ok(!xml.includes('<itunes:category'), 'afwezig is afwezig, ook in een categorie');
+  assert.equal(AP.channelCategory(site()), null);
+  assert.ok(!xml.includes('Achter de poort'));
+});
+
+test('met een OPEN track wordt het Music, aan beide kanten', async () => {
+  db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,?)')
+    .run('m1', 's1', 'open.mp3', 'audio/open.mp3', 'audio/mpeg', 4210688);
+  db.prepare('INSERT INTO audio_tracks (id, site_id, title, artist, duration, media_id, cover_url, fedi_open, created_at) VALUES (?,?,?,?,?,?,?,1,?)')
+    .run('t1', 's1', 'Het nummer', 'De Band', 203, 'm1', '/media/hoes.jpg', '2026-03-01 12:00:00');
+  const xml = await haal();
+  assert.match(xml, /<itunes:category text="Music" \/>/);
+  assert.equal(AP.channelCategory(site()), 'music');
+});
+
+test('een track draagt zijn eigen artiest en hoes mee', async () => {
+  const xml = await haal();
+  assert.match(xml, /<itunes:author>De Band<\/itunes:author>/, 'de artiest van de track, naast die van het kanaal');
+  assert.match(xml, /<itunes:image href="[^"]*\/media\/hoes\.jpg" \/>/);
+  assert.match(xml, /<itunes:duration>203<\/itunes:duration>/);
+});
+
+test.after(() => server.close());
