diff --git a/DeepDrftPublic.Client/Helpers/EmbedSnippetBuilder.cs b/DeepDrftPublic.Client/Helpers/EmbedSnippetBuilder.cs
index 0585d7d..34ab1f4 100644
--- a/DeepDrftPublic.Client/Helpers/EmbedSnippetBuilder.cs
+++ b/DeepDrftPublic.Client/Helpers/EmbedSnippetBuilder.cs
@@ -23,6 +23,17 @@ namespace DeepDrftPublic.Client.Helpers;
/// outer resize is lost. The track snippet needs no script (no panel, no toggle).
///
///
+///
+/// Multi-embed isolation: each call mints a fresh random token (8 hex
+/// chars). The token is used as the iframe id (deepdrft-embed-{token}) and threaded into
+/// the iframe src as &EmbedId={token} so the iframe can learn its own id. The host-side
+/// resize script matches incoming messages on embedId and resizes only the iframe whose id
+/// matches the token — two releases on one host page resize independently without cross-talk. Two
+/// calls for the same release still get distinct tokens, ensuring uniqueness even when the same
+/// release is pasted twice. Older snippets that lack embedId in their postMessage payload are
+/// silently ignored by the script (backward-compatible degradation).
+///
+///
/// Pure string composition so the snippet shape is unit-testable without rendering the component.
///
public static class EmbedSnippetBuilder
@@ -39,14 +50,27 @@ public static class EmbedSnippetBuilder
=> Frame($"{baseUri}FramePlayer?TrackEntryKey={trackEntryKey}", TrackHeight);
public static string ForRelease(string baseUri, string releaseEntryKey)
- => Frame($"{baseUri}FramePlayer?ReleaseEntryKey={releaseEntryKey}", ReleaseHeight, ResizeScript);
+ {
+ // Mint a fresh random token per call so two embeds on the same host page never share an id,
+ // even when they point at the same release.
+ var token = Guid.NewGuid().ToString("N")[..8];
+ var iframeId = $"deepdrft-embed-{token}";
+ var src = $"{baseUri}FramePlayer?ReleaseEntryKey={releaseEntryKey}&EmbedId={token}";
+ return Frame(src, ReleaseHeight, iframeId, ResizeScript(iframeId, token));
+ }
- private static string Frame(string src, int height, string trailingScript = "")
- => $"""{trailingScript}""";
+ private static string Frame(string src, int height, string iframeId = "deepdrft-embed", string trailingScript = "")
+ => $"""{trailingScript}""";
- // Host-side listener: resize #deepdrft-embed when the embedded player posts its panel height. The
- // shape/origin-agnostic guard (ignores non-matching messages) keeps a foreign frame from driving
- // it; the height is clamped to a sane floor so a bad payload can't collapse the player away.
- private const string ResizeScript =
- """""";
+ // Host-side listener: resize the matching iframe when the embedded player posts its panel height.
+ // The embedId field in the payload is matched against the snippet's own token so only this
+ // iframe is driven — foreign frames or other release embeds on the same page cannot interfere.
+ // The height is clamped to a sane floor so a bad payload can't collapse the player away.
+ // Messages without embedId (older snippets) are silently ignored.
+ private static string ResizeScript(string iframeId, string token) =>
+ "";
}
diff --git a/DeepDrftPublic/Interop/embed/embed-frame.ts b/DeepDrftPublic/Interop/embed/embed-frame.ts
index 3f0f98f..03493fd 100644
--- a/DeepDrftPublic/Interop/embed/embed-frame.ts
+++ b/DeepDrftPublic/Interop/embed/embed-frame.ts
@@ -9,15 +9,37 @@
// Degrades safely: if the host page ignores or strips the snippet's listener (Option B's value), the
// panel still renders and toggles inside the iframe — only the outer resize is lost. We post nothing
// when not framed (window === parent), so the docked player is unaffected.
+//
+// Multi-embed isolation: EmbedSnippetBuilder.ForRelease mints a per-snippet random token and passes
+// it as ?EmbedId= in the iframe src. We read it here from window.location.search and include
+// it in the postMessage payload as `embedId`. The host-side resize script matches on this token so
+// only the correct iframe is resized when multiple embeds share a host page. If EmbedId is absent
+// (older already-pasted snippets), embedId is omitted from the payload — those snippets' scripts
+// ignore messages without a matching embedId, so there is no cross-talk either way.
const MESSAGE_TYPE = "deepdrft-embed-resize";
+/** Read the EmbedId query param from the iframe's own URL, if present. */
+function readEmbedId(): string | null {
+ try {
+ return new URLSearchParams(window.location.search).get("EmbedId");
+ } catch {
+ return null;
+ }
+}
+
+// Resolved once at module load — the URL does not change while the iframe is alive.
+const embedId: string | null = readEmbedId();
+
/**
* Measure the live rendered height of the player element and post it to the host page so it can size
* the iframe to match. No-op when not embedded in a frame, or when the element is unmeasurable.
*
* targetOrigin is "*" deliberately: the embedder's origin is unknown (any blog can embed us) and the
* payload carries no secrets — just a height the host is free to ignore.
+ *
+ * The payload includes `embedId` when the iframe src carried an EmbedId query param. The host-side
+ * resize script matches on this field to isolate multiple embeds on the same page.
*/
export function postHeight(element: HTMLElement): void {
if (window.parent === window) return; // Not framed — nothing to resize.
@@ -27,5 +49,7 @@ export function postHeight(element: HTMLElement): void {
const height = Math.ceil(element.getBoundingClientRect().height) + 2;
if (!Number.isFinite(height) || height <= 0) return;
- window.parent.postMessage({ type: MESSAGE_TYPE, height }, "*");
+ const payload: Record = { type: MESSAGE_TYPE, height };
+ if (embedId !== null) payload.embedId = embedId;
+ window.parent.postMessage(payload, "*");
}
diff --git a/DeepDrftTests/EmbedSnippetBuilderTests.cs b/DeepDrftTests/EmbedSnippetBuilderTests.cs
index c55e126..e455bd9 100644
--- a/DeepDrftTests/EmbedSnippetBuilderTests.cs
+++ b/DeepDrftTests/EmbedSnippetBuilderTests.cs
@@ -29,7 +29,8 @@ public class EmbedSnippetBuilderTests
{
var snippet = EmbedSnippetBuilder.ForRelease(BaseUri, "rel-xyz");
- Assert.That(snippet, Does.Contain(@"src=""https://deepdrft.example/FramePlayer?ReleaseEntryKey=rel-xyz"""));
+ // src contains ReleaseEntryKey; may also carry additional query params (e.g. EmbedId).
+ Assert.That(snippet, Does.Contain("ReleaseEntryKey=rel-xyz"));
Assert.That(snippet, Does.Not.Contain("TrackEntryKey"));
}
@@ -94,10 +95,66 @@ public class EmbedSnippetBuilderTests
Assert.That(track, Does.Not.Contain("