Index: src/views/partials/tl-item.ejs
===================================================================
--- src/views/partials/tl-item.ejs	(revision ef1853cad6f220d1c174ec46d96a43d9e5596693)
+++ src/views/partials/tl-item.ejs	(revision 0101d0a23f8210828d4680832a5f1d30b5772d63)
@@ -23,5 +23,19 @@
             <div class="tl-content"><%- emojiHtml(p.content, p.emoji_json) %></div>
           <% } %>
-          <% var _quote = noteQuote(p.quote_json); if (_quote) { %><%- include('../partials/quote-card', { q: _quote }) %><% } %>
+          <%
+            // One card for both: a fediverse quote and an external embed look
+            // identical; only where they came from differs. An embed carries a
+            // title and a thumbnail, never an iframe.
+            var _quote = noteQuote(p.quote_json);
+            if (!_quote) {
+              var _emb = noteQuote(p.embed_json);
+              // The title comes from a third-party oEmbed provider, so it is
+              // escaped here: the quote card renders `content` as HTML (fine for
+              // AP content, which we sanitise on the way in, but not for this).
+              if (_emb) _quote = { url: _emb.url, author: _emb.author || (_emb.provider ? { name: _emb.provider } : null),
+                                   content: _emb.title ? ('<p>' + emojiName(_emb.title, null) + '</p>') : '', media: _emb.media || [] };
+            }
+          %>
+          <% if (_quote) { %><%- include('../partials/quote-card', { q: _quote }) %><% } %>
           <% if (_nsfwVisual) { %><div class="nsfw-media"><% } %>
           <% if (imgs.length) { %>
