diff --git a/interactions.njk b/interactions.njk
index 0b6d0be..04344bd 100644
--- a/interactions.njk
+++ b/interactions.njk
@@ -243,6 +243,14 @@ permalink: /interactions/
🔖 bookmarked
+ {# Platform badge (from conversations API) #}
+
+
+
+
+
+
+
@@ -357,38 +365,47 @@ function interactionsApp() {
this.error = null;
try {
- // Use our server-side proxy which has the token
- const url = `/webmentions/api/mentions?per-page=${this.perPage}&page=${this.page}`;
+ // Fetch from both webmention-io and conversations APIs in parallel
+ const wmUrl = `/webmentions/api/mentions?per-page=${this.perPage}&page=${this.page}`;
+ const convUrl = `/conversations/api/mentions?per-page=${this.perPage}&page=${this.page}`;
- const response = await fetch(url);
- if (!response.ok) {
- if (response.status === 404) {
- this.notConfigured = true;
- return;
- }
- const data = await response.json().catch(() => ({}));
- throw new Error(data.message || `HTTP ${response.status}`);
+ const [wmResult, convResult] = await Promise.allSettled([
+ fetch(wmUrl).then(r => {
+ if (r.status === 404) return { children: [], notConfigured: true };
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
+ return r.json();
+ }),
+ fetch(convUrl).then(r => {
+ if (!r.ok) return { children: [] };
+ return r.json();
+ }).catch(() => ({ children: [] })),
+ ]);
+
+ const wmData = wmResult.status === 'fulfilled' ? wmResult.value : { children: [] };
+ const convData = convResult.status === 'fulfilled' ? convResult.value : { children: [] };
+
+ // Check if webmention-io is configured
+ if (wmData.notConfigured && (!convData.children || !convData.children.length)) {
+ this.notConfigured = true;
+ return;
}
this.notConfigured = false;
- const data = await response.json();
- const newMentions = data.children || [];
+ // Merge and deduplicate - conversations items (with platform field) take priority
+ const merged = this.mergeAndDeduplicate(
+ wmData.children || [],
+ convData.children || []
+ );
- // Sort by published date, newest first
- newMentions.sort((a, b) => {
+ // Sort by date, newest first
+ merged.sort((a, b) => {
const dateA = new Date(a.published || a['wm-received'] || 0);
const dateB = new Date(b.published || b['wm-received'] || 0);
return dateB - dateA;
});
- if (silent) {
- // For silent refresh, replace all
- this.webmentions = newMentions;
- } else {
- this.webmentions = newMentions;
- }
-
- this.hasMore = newMentions.length === this.perPage;
+ this.webmentions = merged;
+ this.hasMore = (wmData.children || []).length === this.perPage;
} catch (err) {
this.error = `Failed to load webmentions: ${err.message}`;
console.error('[Interactions]', err);
@@ -397,20 +414,60 @@ function interactionsApp() {
}
},
+ mergeAndDeduplicate(wmItems, convItems) {
+ // Build a Set of source URLs from conversations for dedup
+ const convUrls = new Set(convItems.map(c => c.url).filter(Boolean));
+ const seen = new Set();
+ const result = [];
+
+ // Add all conversations items first (they have richer metadata)
+ for (const item of convItems) {
+ const key = item['wm-id'] || item.url;
+ if (key && !seen.has(key)) {
+ seen.add(key);
+ result.push(item);
+ }
+ }
+
+ // Add webmention-io items that aren't duplicated by conversations
+ for (const item of wmItems) {
+ const wmKey = item['wm-id'];
+ if (seen.has(wmKey)) continue;
+
+ // Also check if this webmention's source URL matches a conversations item
+ // (same interaction from Bridgy webmention AND direct poll)
+ if (item.url && convUrls.has(item.url)) continue;
+
+ seen.add(wmKey);
+ result.push(item);
+ }
+
+ return result;
+ },
+
async loadMore() {
this.loadingMore = true;
this.page++;
try {
- const url = `/webmentions/api/mentions?per-page=${this.perPage}&page=${this.page}`;
- const response = await fetch(url);
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
+ const wmUrl = `/webmentions/api/mentions?per-page=${this.perPage}&page=${this.page}`;
+ const convUrl = `/conversations/api/mentions?per-page=${this.perPage}&page=${this.page}`;
- const data = await response.json();
- const newMentions = data.children || [];
+ const [wmResult, convResult] = await Promise.allSettled([
+ fetch(wmUrl).then(r => r.ok ? r.json() : { children: [] }),
+ fetch(convUrl).then(r => r.ok ? r.json() : { children: [] }).catch(() => ({ children: [] })),
+ ]);
- this.webmentions = [...this.webmentions, ...newMentions];
- this.hasMore = newMentions.length === this.perPage;
+ const wmData = wmResult.status === 'fulfilled' ? wmResult.value : { children: [] };
+ const convData = convResult.status === 'fulfilled' ? convResult.value : { children: [] };
+
+ const merged = this.mergeAndDeduplicate(
+ wmData.children || [],
+ convData.children || []
+ );
+
+ this.webmentions = [...this.webmentions, ...merged];
+ this.hasMore = (wmData.children || []).length === this.perPage;
} catch (err) {
this.error = `Failed to load more: ${err.message}`;
} finally {
diff --git a/js/webmentions.js b/js/webmentions.js
index 72f522e..3f69f0f 100644
--- a/js/webmentions.js
+++ b/js/webmentions.js
@@ -109,22 +109,46 @@
processWebmentions(cached);
}
- // Always fetch fresh data (updates cache for next refresh)
+ // Conversations API URLs (dual-fetch for enriched data)
+ const convApiUrl1 = `/conversations/api/mentions?target=${encodeURIComponent(targetWithSlash)}&per-page=100`;
+ const convApiUrl2 = `/conversations/api/mentions?target=${encodeURIComponent(targetWithoutSlash)}&per-page=100`;
+
+ // Always fetch fresh data from both APIs (updates cache for next refresh)
Promise.all([
fetch(apiUrl1).then((res) => res.json()).catch(() => ({ children: [] })),
fetch(apiUrl2).then((res) => res.json()).catch(() => ({ children: [] })),
+ fetch(convApiUrl1).then((res) => res.ok ? res.json() : { children: [] }).catch(() => ({ children: [] })),
+ fetch(convApiUrl2).then((res) => res.ok ? res.json() : { children: [] }).catch(() => ({ children: [] })),
])
- .then(([data1, data2]) => {
- // Merge and deduplicate by wm-id
+ .then(([wmData1, wmData2, convData1, convData2]) => {
+ // Collect all items from both APIs
+ const wmItems = [...(wmData1.children || []), ...(wmData2.children || [])];
+ const convItems = [...(convData1.children || []), ...(convData2.children || [])];
+
+ // Build dedup sets from conversations items (richer metadata, take priority)
+ const convUrls = new Set(convItems.map(c => c.url).filter(Boolean));
const seen = new Set();
const allChildren = [];
- for (const wm of [...(data1.children || []), ...(data2.children || [])]) {
- if (!seen.has(wm['wm-id'])) {
- seen.add(wm['wm-id']);
+
+ // Add conversations items first (they have platform provenance)
+ for (const wm of convItems) {
+ const key = wm['wm-id'] || wm.url;
+ if (key && !seen.has(key)) {
+ seen.add(key);
allChildren.push(wm);
}
}
+ // Add webmention-io items, skipping duplicates
+ for (const wm of wmItems) {
+ const key = wm['wm-id'];
+ if (seen.has(key)) continue;
+ // Also skip if same source URL exists in conversations
+ if (wm.url && convUrls.has(wm.url)) continue;
+ seen.add(key);
+ allChildren.push(wm);
+ }
+
// Cache the merged results
setCachedData(allChildren);