Index: src/routes/embed.js
===================================================================
--- src/routes/embed.js	(revision e951b7c4efb52daa3b8964b528fd3f604de85365)
+++ src/routes/embed.js	(revision 4c9c60bcdb47e6d3cc3b4b710bfb61477bb9f554)
@@ -12,4 +12,5 @@
 
 import express from 'express';
+import db from '../config/database.js';
 import { premiumUnlocked } from '../services/PatreonService.js';
 
@@ -28,7 +29,28 @@
   );
 
-  const tracks = (res.locals.audioTracks || []).map((t) => ({
+  let tracks = (res.locals.audioTracks || []).map((t) => ({
     id: t.id, title: t.title, artist: t.artist, duration: t.duration, url: t.media_url,
   })).filter((t) => t.url);
+
+  // ?post=<slug> → scope the player to that post's tracks (for the fediverse
+  // player card). Resolve [[track]]/[[album]]/[[playlist]] shortcodes → track ids.
+  const postSlug = (req.query.post || '').toString();
+  if (postSlug) {
+    try {
+      const post = db.prepare("SELECT content FROM posts WHERE site_id = ? AND slug = ? AND status = 'published'").get(site.id, postSlug);
+      if (post && post.content) {
+        const ids = []; const seen = new Set();
+        const add = (id) => { if (id && !seen.has(id)) { seen.add(id); ids.push(id); } };
+        for (const m of post.content.matchAll(/\[\[track:([A-Za-z0-9_-]+)\]\]/g)) add(m[1]);
+        for (const m of post.content.matchAll(/\[\[album:([^\]]+)\]\]/g)) for (const r of db.prepare('SELECT id FROM audio_tracks WHERE site_id = ? AND album = ? ORDER BY position').all(site.id, m[1].trim())) add(r.id);
+        for (const m of post.content.matchAll(/\[\[playlist:([A-Za-z0-9_-]+)\]\]/g)) for (const r of db.prepare('SELECT track_id FROM playlist_tracks WHERE playlist_id = ? ORDER BY position').all(m[1])) add(r.track_id);
+        if (ids.length) {
+          const byId = new Map(tracks.map((t) => [t.id, t]));
+          const scoped = ids.map((id) => byId.get(id)).filter(Boolean);
+          if (scoped.length) tracks = scoped;
+        }
+      }
+    } catch { /* fall back to the full site player */ }
+  }
 
   res.render('pages/embed-player', {
Index: src/views/shell.ejs
===================================================================
--- src/views/shell.ejs	(revision e951b7c4efb52daa3b8964b528fd3f604de85365)
+++ src/views/shell.ejs	(revision 4c9c60bcdb47e6d3cc3b4b710bfb61477bb9f554)
@@ -153,4 +153,22 @@
 <% } %>
 <% if (_canonical) { %><meta property="og:url" content="<%= _e(_canonical) %>"><% } %>
+<%
+// Fediverse/social PLAYER card for posts with audio: instead of shipping the raw
+// mp3, point at our embeddable player (/embed?post=slug) so Mastodon shows an
+// inline player that streams via the gated /audio/stream (no downloadable file).
+const _postAudio = !!(typeof post !== 'undefined' && post
+  && /\[\[(track|album|playlist):/i.test(post.content || post.content_html || '')
+  && typeof premiumUnlocked !== 'undefined' && premiumUnlocked);
+const _embedUrl = _postAudio
+  ? ((typeof ogOrigin !== 'undefined' && ogOrigin ? ogOrigin : '') + safeUrlBase + '/embed?post=' + encodeURIComponent(post.slug))
+  : '';
+%>
+<% if (_postAudio) { %>
+<meta property="og:video"            content="<%= _e(_embedUrl) %>">
+<meta property="og:video:secure_url" content="<%= _e(_embedUrl) %>">
+<meta property="og:video:type"       content="text/html">
+<meta property="og:video:width"      content="480">
+<meta property="og:video:height"     content="160">
+<% } %>
 <% if (typeof post !== 'undefined' && post && post.published_at) { %>
 <meta property="article:published_time" content="<%= _e(post.published_at) %>">
@@ -160,5 +178,10 @@
 
 <!-- Twitter Cards -->
-<meta name="twitter:card"        content="<%= _socialImage ? 'summary_large_image' : 'summary' %>">
+<meta name="twitter:card"        content="<%= _postAudio ? 'player' : (_socialImage ? 'summary_large_image' : 'summary') %>">
+<% if (_postAudio) { %>
+<meta name="twitter:player"        content="<%= _e(_embedUrl) %>">
+<meta name="twitter:player:width"  content="480">
+<meta name="twitter:player:height" content="160">
+<% } %>
 <meta name="twitter:title"       content="<%= _e(_socialTitle) %>">
 <meta name="twitter:description" content="<%= _e(_socialDescr) %>">
