fix: robust mastodon dedup with MutationObserver + URL normalization
Build & Deploy / build-and-deploy (push) Successful in 1m59s
Build & Deploy / build-and-deploy (push) Successful in 1m59s
- MutationObserver set up before fetching so webmention NEW cards added after the dedup pass are also removed immediately (race condition fix) - Normalize /@handle and /users/handle URL variants to same form before comparing — covers Mastodon instances that use either format - Add wm-owner-reply-slot div + Reply link to bsky/mastodon Alpine cards to match build-time webmention card structure
This commit is contained in:
@@ -228,8 +228,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="text-surface-700 dark:text-surface-300 prose dark:prose-invert prose-sm max-w-none"
|
<div class="text-surface-700 dark:text-surface-300 prose dark:prose-invert prose-sm max-w-none"
|
||||||
x-text="reply.text"></div>
|
x-text="reply.text"></div>
|
||||||
|
<a :href="reply.url"
|
||||||
|
class="wm-reply-btn hidden text-xs text-primary-600 dark:text-primary-400 hover:underline mt-2"
|
||||||
|
target="_blank" rel="noopener">Reply on Bluesky</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="wm-owner-reply-slot ml-13 mt-2"></div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -258,8 +262,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="text-surface-700 dark:text-surface-300 prose dark:prose-invert prose-sm max-w-none"
|
<div class="text-surface-700 dark:text-surface-300 prose dark:prose-invert prose-sm max-w-none"
|
||||||
x-html="reply.content"></div>
|
x-html="reply.content"></div>
|
||||||
|
<a :href="reply.url"
|
||||||
|
class="wm-reply-btn hidden text-xs text-primary-600 dark:text-primary-400 hover:underline mt-2"
|
||||||
|
target="_blank" rel="noopener">Reply on Mastodon</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="wm-owner-reply-slot ml-13 mt-2"></div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
+33
-4
@@ -8,7 +8,34 @@ document.addEventListener("alpine:init", () => {
|
|||||||
mastodonReplies: [],
|
mastodonReplies: [],
|
||||||
loading: true,
|
loading: true,
|
||||||
|
|
||||||
|
// Normalized mastodon author URLs for dedup — populated after _fetchMastodon
|
||||||
|
_mastodonAuthorNormed: new Set(),
|
||||||
|
|
||||||
|
// Normalize /@handle and /users/handle to the same form for URL comparison
|
||||||
|
_normalizeActorUrl(url) {
|
||||||
|
return (url || "").replace(/\/(users\/|@)/, "/").replace(/\/$/, "").toLowerCase();
|
||||||
|
},
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
// Watch for webmention cards added AFTER our dedup pass (race condition with webmentions.js)
|
||||||
|
const repliesList = this.$el.querySelector("ul");
|
||||||
|
if (repliesList) {
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
for (const m of mutations) {
|
||||||
|
for (const node of m.addedNodes) {
|
||||||
|
if (node.nodeType === 1 && node.dataset.authorUrl) {
|
||||||
|
if (this._mastodonAuthorNormed.has(this._normalizeActorUrl(node.dataset.authorUrl))) {
|
||||||
|
node.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(repliesList, { childList: true });
|
||||||
|
// Disconnect after page is hidden — observer is no longer needed
|
||||||
|
document.addEventListener("visibilitychange", () => observer.disconnect(), { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
const tasks = [];
|
const tasks = [];
|
||||||
if (bskyUrl) tasks.push(this._fetchBsky(bskyUrl));
|
if (bskyUrl) tasks.push(this._fetchBsky(bskyUrl));
|
||||||
if (mastodonInstance && apIdentity) tasks.push(this._fetchMastodon(postUrl, mastodonInstance, apIdentity));
|
if (mastodonInstance && apIdentity) tasks.push(this._fetchMastodon(postUrl, mastodonInstance, apIdentity));
|
||||||
@@ -98,11 +125,13 @@ document.addEventListener("alpine:init", () => {
|
|||||||
favourites: s.favourites_count || 0,
|
favourites: s.favourites_count || 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Remove webmention cards (build-time or client-fetched) that duplicate mastodon replies.
|
// Build normalized author URL set for dedup (covers /@handle and /users/handle variants)
|
||||||
// Webmention cards have data-author-url; Alpine x-for cards do not.
|
this._mastodonAuthorNormed = new Set(
|
||||||
const mastodonAuthorUrls = new Set(this.mastodonReplies.map((r) => r.author.url));
|
this.mastodonReplies.map((r) => this._normalizeActorUrl(r.author.url))
|
||||||
|
);
|
||||||
|
// Remove webmention cards (build-time or NEW) that duplicate mastodon replies
|
||||||
document.querySelectorAll(".webmention-replies li[data-author-url]").forEach((li) => {
|
document.querySelectorAll(".webmention-replies li[data-author-url]").forEach((li) => {
|
||||||
if (mastodonAuthorUrls.has(li.dataset.authorUrl)) li.remove();
|
if (this._mastodonAuthorNormed.has(this._normalizeActorUrl(li.dataset.authorUrl))) li.remove();
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
// fail silently
|
// fail silently
|
||||||
|
|||||||
Reference in New Issue
Block a user