mirror of
https://github.com/svemagie/blog-eleventy-indiekit.git
synced 2026-04-02 16:44:56 +02:00
feat: blog filter nav, interactions pagination, note unfurl, pagefind improvements
- Replace broken client-side type filter on /blog/ with navigation pill links to dedicated collection pages (with post counts) - Replace Load More with proper prev/next/page-number pagination on Interactions inbound tab (20 per page, filter resets page) - Add auto-unfurl transform for standalone external links in notes - Exclude Digest and Categories pages from Pagefind search index - Add Pagefind search filters for post type, year, and category - Add Pagefind filter metadata to page.njk layout Confab-Link: http://localhost:8080/sessions/956f4251-b4a9-4bc9-b214-53402ad1fe63
This commit is contained in:
@@ -133,4 +133,18 @@ withSidebar: true
|
|||||||
{# Hidden metadata for microformats #}
|
{# Hidden metadata for microformats #}
|
||||||
<a class="u-url hidden" href="{{ page.url }}"></a>
|
<a class="u-url hidden" href="{{ page.url }}"></a>
|
||||||
<data class="p-author h-card hidden" value="{{ site.author.name }}"></data>
|
<data class="p-author h-card hidden" value="{{ site.author.name }}"></data>
|
||||||
|
|
||||||
|
{# Pagefind filter metadata #}
|
||||||
|
<div hidden>
|
||||||
|
<span data-pagefind-filter="type">Page</span>
|
||||||
|
{% if category %}
|
||||||
|
{% if category is string %}
|
||||||
|
<span data-pagefind-filter="category">{{ category }}</span>
|
||||||
|
{% else %}
|
||||||
|
{% for cat in category %}
|
||||||
|
<span data-pagefind-filter="category">{{ cat }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
25
blog.njk
25
blog.njk
@@ -22,20 +22,13 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% if paginatedPosts.length > 0 %}
|
{% if paginatedPosts.length > 0 %}
|
||||||
<filter-container oninit leave-url-alone>
|
<nav class="flex flex-wrap gap-2 mb-6" aria-label="Filter by post type">
|
||||||
<div class="flex flex-wrap gap-3 mb-6">
|
<a href="/blog/" class="px-3 py-1.5 text-sm font-medium rounded-full bg-accent-600 text-white dark:bg-accent-500">All Posts <span class="opacity-75">({{ collections.posts.length }})</span></a>
|
||||||
<select data-filter-key="type" class="px-3 py-1.5 text-sm bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 rounded-lg">
|
{% for pt in enabledPostTypes %}
|
||||||
<option value="">All Types</option>
|
{% set collName = pt.label | lower %}
|
||||||
<option value="article">Articles</option>
|
<a href="{{ pt.path }}" class="px-3 py-1.5 text-sm font-medium rounded-full bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-300 hover:bg-surface-200 dark:hover:bg-surface-700 border border-surface-200 dark:border-surface-700 transition-colors">{{ pt.label }}{% if collections[collName] %} <span class="text-surface-400 dark:text-surface-500">({{ collections[collName].length }})</span>{% endif %}</a>
|
||||||
<option value="note">Notes</option>
|
{% endfor %}
|
||||||
<option value="photo">Photos</option>
|
</nav>
|
||||||
<option value="bookmark">Bookmarks</option>
|
|
||||||
<option value="like">Likes</option>
|
|
||||||
<option value="reply">Replies</option>
|
|
||||||
<option value="repost">Reposts</option>
|
|
||||||
</select>
|
|
||||||
<span data-filter-results class="text-sm text-surface-500 dark:text-surface-400 self-center"></span>
|
|
||||||
</div>
|
|
||||||
<ul class="post-list">
|
<ul class="post-list">
|
||||||
{% for post in paginatedPosts %}
|
{% for post in paginatedPosts %}
|
||||||
{# Detect post type from frontmatter properties #}
|
{# Detect post type from frontmatter properties #}
|
||||||
@@ -44,7 +37,6 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
|
|||||||
{% set repostedUrl = post.data.repostOf or post.data.repost_of %}
|
{% set repostedUrl = post.data.repostOf or post.data.repost_of %}
|
||||||
{% set replyToUrl = post.data.inReplyTo or post.data.in_reply_to %}
|
{% set replyToUrl = post.data.inReplyTo or post.data.in_reply_to %}
|
||||||
{% set hasPhotos = post.data.photo and post.data.photo.length %}
|
{% set hasPhotos = post.data.photo and post.data.photo.length %}
|
||||||
{% set _postType %}{% if likedUrl %}like{% elif bookmarkedUrl %}bookmark{% elif repostedUrl %}repost{% elif replyToUrl %}reply{% elif hasPhotos %}photo{% elif post.data.title %}article{% else %}note{% endif %}{% endset %}
|
|
||||||
{% set borderClass = "" %}
|
{% set borderClass = "" %}
|
||||||
{% if likedUrl %}
|
{% if likedUrl %}
|
||||||
{% set borderClass = "border-l-[3px] border-l-red-400 dark:border-l-red-500" %}
|
{% set borderClass = "border-l-[3px] border-l-red-400 dark:border-l-red-500" %}
|
||||||
@@ -59,7 +51,7 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% set borderClass = "border-l-[3px] border-l-surface-300 dark:border-l-surface-600" %}
|
{% set borderClass = "border-l-[3px] border-l-surface-300 dark:border-l-surface-600" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="h-entry post-card {{ borderClass }}" data-filter-type="{{ _postType | trim }}">
|
<li class="h-entry post-card {{ borderClass }}">
|
||||||
|
|
||||||
{% if likedUrl %}
|
{% if likedUrl %}
|
||||||
{# ── Like card ── #}
|
{# ── Like card ── #}
|
||||||
@@ -341,7 +333,6 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
|
|||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</filter-container>
|
|
||||||
|
|
||||||
{# Pagination controls #}
|
{# Pagination controls #}
|
||||||
{% if pagination.pages.length > 1 %}
|
{% if pagination.pages.length > 1 %}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
layout: layouts/base.njk
|
layout: layouts/base.njk
|
||||||
title: Categories
|
title: Categories
|
||||||
withSidebar: true
|
withSidebar: true
|
||||||
|
pagefindIgnore: true
|
||||||
permalink: categories/
|
permalink: categories/
|
||||||
eleventyImport:
|
eleventyImport:
|
||||||
collections:
|
collections:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
layout: layouts/base.njk
|
layout: layouts/base.njk
|
||||||
withSidebar: true
|
withSidebar: true
|
||||||
|
pagefindIgnore: true
|
||||||
pagination:
|
pagination:
|
||||||
data: collections.categories
|
data: collections.categories
|
||||||
size: 1
|
size: 1
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
layout: layouts/base.njk
|
layout: layouts/base.njk
|
||||||
title: Weekly Digest
|
title: Weekly Digest
|
||||||
withSidebar: true
|
withSidebar: true
|
||||||
|
pagefindIgnore: true
|
||||||
eleventyExcludeFromCollections: true
|
eleventyExcludeFromCollections: true
|
||||||
eleventyImport:
|
eleventyImport:
|
||||||
collections:
|
collections:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
layout: layouts/base.njk
|
layout: layouts/base.njk
|
||||||
withSidebar: true
|
withSidebar: true
|
||||||
|
pagefindIgnore: true
|
||||||
eleventyExcludeFromCollections: true
|
eleventyExcludeFromCollections: true
|
||||||
eleventyImport:
|
eleventyImport:
|
||||||
collections:
|
collections:
|
||||||
|
|||||||
@@ -386,6 +386,52 @@ export default function (eleventyConfig) {
|
|||||||
return content;
|
return content;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Auto-unfurl standalone external links in note content
|
||||||
|
// Finds <a> tags that are the primary content of a <p> tag and injects OG preview cards
|
||||||
|
eleventyConfig.addTransform("auto-unfurl-notes", async function (content, outputPath) {
|
||||||
|
if (!outputPath || !outputPath.endsWith(".html")) return content;
|
||||||
|
// Only process note pages (individual + listing)
|
||||||
|
if (!outputPath.includes("/notes/")) return content;
|
||||||
|
|
||||||
|
// Match <p> tags whose content is short text + a single external <a> as the last element
|
||||||
|
// Pattern: <p>optional short text <a href="https://external.example">...</a></p>
|
||||||
|
const linkParagraphRe = /<p>([^<]{0,80})?<a\s+href="(https?:\/\/[^"]+)"[^>]*>[^<]*<\/a>\s*<\/p>/g;
|
||||||
|
const siteHost = new URL(siteUrl).hostname;
|
||||||
|
const matches = [];
|
||||||
|
|
||||||
|
let match;
|
||||||
|
while ((match = linkParagraphRe.exec(content)) !== null) {
|
||||||
|
const url = match[2];
|
||||||
|
try {
|
||||||
|
const linkHost = new URL(url).hostname;
|
||||||
|
// Skip same-domain links and common non-content URLs
|
||||||
|
if (linkHost === siteHost || linkHost.endsWith("." + siteHost)) continue;
|
||||||
|
matches.push({ fullMatch: match[0], url, index: match.index });
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches.length === 0) return content;
|
||||||
|
|
||||||
|
// Unfurl all matched URLs in parallel (uses cache, throttles network)
|
||||||
|
const cards = await Promise.all(matches.map(m => prefetchUrl(m.url)));
|
||||||
|
|
||||||
|
// Replace in reverse order to preserve indices
|
||||||
|
let result = content;
|
||||||
|
for (let i = matches.length - 1; i >= 0; i--) {
|
||||||
|
const m = matches[i];
|
||||||
|
const card = cards[i];
|
||||||
|
// Skip if unfurl returned just a fallback link (no OG data)
|
||||||
|
if (!card || !card.includes("unfurl-card")) continue;
|
||||||
|
// Insert the unfurl card after the paragraph
|
||||||
|
const insertPos = m.index + m.fullMatch.length;
|
||||||
|
result = result.slice(0, insertPos) + "\n" + card + "\n" + result.slice(insertPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
// HTML minification — only during initial build, skip during watch rebuilds
|
// HTML minification — only during initial build, skip during watch rebuilds
|
||||||
eleventyConfig.addTransform("htmlmin", async function (content, outputPath) {
|
eleventyConfig.addTransform("htmlmin", async function (content, outputPath) {
|
||||||
if (outputPath && outputPath.endsWith(".html") && process.env.ELEVENTY_RUN_MODE === "build") {
|
if (outputPath && outputPath.endsWith(".html") && process.env.ELEVENTY_RUN_MODE === "build") {
|
||||||
|
|||||||
167
interactions.njk
167
interactions.njk
@@ -283,14 +283,43 @@ permalink: /interactions/
|
|||||||
<p>No webmentions found for this filter.</p>
|
<p>No webmentions found for this filter.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Pagination / Load more #}
|
{# Pagination controls #}
|
||||||
<div x-show="hasMore" class="text-center pt-4">
|
<div x-show="totalPages > 1" class="pt-6">
|
||||||
|
<nav class="pagination" aria-label="Webmentions pagination">
|
||||||
|
<div class="pagination-info">
|
||||||
|
Page <span x-text="currentPage"></span> of <span x-text="totalPages"></span>
|
||||||
|
<span class="text-surface-400 dark:text-surface-500 ml-1">(<span x-text="filteredWebmentions.length"></span> total)</span>
|
||||||
|
</div>
|
||||||
|
<div class="pagination-links">
|
||||||
<button
|
<button
|
||||||
@click="loadMore()"
|
@click="goToPage(currentPage - 1)"
|
||||||
:disabled="loadingMore"
|
:disabled="currentPage <= 1"
|
||||||
class="px-4 py-2 bg-surface-100 dark:bg-surface-800 hover:bg-surface-200 dark:hover:bg-surface-700 rounded-lg text-sm transition-colors disabled:opacity-50">
|
class="pagination-link"
|
||||||
<span x-text="loadingMore ? 'Loading...' : 'Load More'"></span>
|
:class="currentPage <= 1 ? 'disabled' : ''">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
|
||||||
|
Previous
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<template x-for="p in pageNumbers" :key="p">
|
||||||
|
<button
|
||||||
|
@click="typeof p === 'number' && goToPage(p)"
|
||||||
|
:disabled="p === '…'"
|
||||||
|
:class="p === currentPage ? 'bg-accent-600 text-white dark:bg-accent-500' : p === '…' ? 'cursor-default opacity-50' : 'bg-surface-100 dark:bg-surface-800 hover:bg-surface-200 dark:hover:bg-surface-700'"
|
||||||
|
class="px-3 py-1.5 text-sm rounded-lg transition-colors"
|
||||||
|
x-text="p">
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="goToPage(currentPage + 1)"
|
||||||
|
:disabled="currentPage >= totalPages"
|
||||||
|
class="pagination-link"
|
||||||
|
:class="currentPage >= totalPages ? 'disabled' : ''">
|
||||||
|
Next
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -316,14 +345,13 @@ function interactionsApp() {
|
|||||||
return {
|
return {
|
||||||
activeTab: 'inbound',
|
activeTab: 'inbound',
|
||||||
loading: false,
|
loading: false,
|
||||||
loadingMore: false,
|
|
||||||
error: null,
|
error: null,
|
||||||
notConfigured: false,
|
notConfigured: false,
|
||||||
webmentions: [],
|
webmentions: [],
|
||||||
filterType: 'all',
|
filterType: 'all',
|
||||||
page: 0,
|
currentPage: 1,
|
||||||
perPage: 50,
|
displayPerPage: 20,
|
||||||
hasMore: true,
|
fetchPerPage: 200,
|
||||||
refreshInterval: null,
|
refreshInterval: null,
|
||||||
|
|
||||||
get likes() {
|
get likes() {
|
||||||
@@ -351,14 +379,41 @@ function interactionsApp() {
|
|||||||
return this.webmentions.filter(wm => wm['wm-property'] === this.filterType);
|
return this.webmentions.filter(wm => wm['wm-property'] === this.filterType);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get totalPages() {
|
||||||
|
return Math.max(1, Math.ceil(this.filteredWebmentions.length / this.displayPerPage));
|
||||||
|
},
|
||||||
|
|
||||||
get paginatedWebmentions() {
|
get paginatedWebmentions() {
|
||||||
// Client-side pagination of filtered results
|
const start = (this.currentPage - 1) * this.displayPerPage;
|
||||||
return this.filteredWebmentions;
|
return this.filteredWebmentions.slice(start, start + this.displayPerPage);
|
||||||
|
},
|
||||||
|
|
||||||
|
get pageNumbers() {
|
||||||
|
const total = this.totalPages;
|
||||||
|
const current = this.currentPage;
|
||||||
|
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
|
||||||
|
const pages = [];
|
||||||
|
pages.push(1);
|
||||||
|
if (current > 3) pages.push('…');
|
||||||
|
for (let i = Math.max(2, current - 1); i <= Math.min(total - 1, current + 1); i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
if (current < total - 2) pages.push('…');
|
||||||
|
pages.push(total);
|
||||||
|
return pages;
|
||||||
|
},
|
||||||
|
|
||||||
|
goToPage(page) {
|
||||||
|
if (page < 1 || page > this.totalPages) return;
|
||||||
|
this.currentPage = page;
|
||||||
|
// Scroll to top of inbound tab
|
||||||
|
this.$el.closest('[x-show]')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
},
|
},
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
// Reset page when filter changes
|
||||||
|
this.$watch('filterType', () => { this.currentPage = 1; });
|
||||||
await this.fetchWebmentions();
|
await this.fetchWebmentions();
|
||||||
// Auto-refresh every 5 minutes (skip if not configured)
|
|
||||||
if (!this.notConfigured) {
|
if (!this.notConfigured) {
|
||||||
this.refreshInterval = setInterval(() => this.fetchWebmentions(true), 5 * 60 * 1000);
|
this.refreshInterval = setInterval(() => this.fetchWebmentions(true), 5 * 60 * 1000);
|
||||||
}
|
}
|
||||||
@@ -367,24 +422,27 @@ function interactionsApp() {
|
|||||||
async fetchWebmentions(silent = false) {
|
async fetchWebmentions(silent = false) {
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.page = 0;
|
|
||||||
this.webmentions = [];
|
this.webmentions = [];
|
||||||
this.hasMore = true;
|
|
||||||
}
|
}
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch from both webmention-io and conversations APIs in parallel
|
// Fetch all available webmentions by paging through the APIs
|
||||||
const wmUrl = `/webmentions/api/mentions?per-page=${this.perPage}&page=${this.page}`;
|
let allWm = [];
|
||||||
const convUrl = `/conversations/api/mentions?per-page=${this.perPage}&page=${this.page}`;
|
let allConv = [];
|
||||||
|
let wmPage = 0;
|
||||||
|
let wmDone = false;
|
||||||
|
let wmConfigured = true;
|
||||||
|
|
||||||
|
// Fetch all pages from both APIs
|
||||||
|
while (!wmDone) {
|
||||||
const [wmResult, convResult] = await Promise.allSettled([
|
const [wmResult, convResult] = await Promise.allSettled([
|
||||||
fetch(wmUrl).then(r => {
|
fetch(`/webmentions/api/mentions?per-page=${this.fetchPerPage}&page=${wmPage}`).then(r => {
|
||||||
if (r.status === 404) return { children: [], notConfigured: true };
|
if (r.status === 404) return { children: [], notConfigured: true };
|
||||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||||
return r.json();
|
return r.json();
|
||||||
}),
|
}),
|
||||||
fetch(convUrl).then(r => {
|
fetch(`/conversations/api/mentions?per-page=${this.fetchPerPage}&page=${wmPage}`).then(r => {
|
||||||
if (!r.ok) return { children: [] };
|
if (!r.ok) return { children: [] };
|
||||||
return r.json();
|
return r.json();
|
||||||
}).catch(() => ({ children: [] })),
|
}).catch(() => ({ children: [] })),
|
||||||
@@ -393,20 +451,31 @@ function interactionsApp() {
|
|||||||
const wmData = wmResult.status === 'fulfilled' ? wmResult.value : { children: [] };
|
const wmData = wmResult.status === 'fulfilled' ? wmResult.value : { children: [] };
|
||||||
const convData = convResult.status === 'fulfilled' ? convResult.value : { children: [] };
|
const convData = convResult.status === 'fulfilled' ? convResult.value : { children: [] };
|
||||||
|
|
||||||
// Check if webmention-io is configured
|
if (wmData.notConfigured) wmConfigured = false;
|
||||||
if (wmData.notConfigured && (!convData.children || !convData.children.length)) {
|
|
||||||
|
allWm = allWm.concat(wmData.children || []);
|
||||||
|
allConv = allConv.concat(convData.children || []);
|
||||||
|
|
||||||
|
// Stop if both APIs returned fewer than a full page
|
||||||
|
const wmCount = (wmData.children || []).length;
|
||||||
|
const convCount = (convData.children || []).length;
|
||||||
|
if (wmCount < this.fetchPerPage && convCount < this.fetchPerPage) {
|
||||||
|
wmDone = true;
|
||||||
|
} else {
|
||||||
|
wmPage++;
|
||||||
|
// Safety cap to prevent infinite loops
|
||||||
|
if (wmPage > 20) wmDone = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wmConfigured && allConv.length === 0) {
|
||||||
this.notConfigured = true;
|
this.notConfigured = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.notConfigured = false;
|
this.notConfigured = false;
|
||||||
|
|
||||||
// Merge and deduplicate - conversations items (with platform field) take priority
|
const merged = this.mergeAndDeduplicate(allWm, allConv);
|
||||||
const merged = this.mergeAndDeduplicate(
|
|
||||||
wmData.children || [],
|
|
||||||
convData.children || []
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sort by date, newest first
|
|
||||||
merged.sort((a, b) => {
|
merged.sort((a, b) => {
|
||||||
const dateA = new Date(a.published || a['wm-received'] || 0);
|
const dateA = new Date(a.published || a['wm-received'] || 0);
|
||||||
const dateB = new Date(b.published || b['wm-received'] || 0);
|
const dateB = new Date(b.published || b['wm-received'] || 0);
|
||||||
@@ -414,7 +483,7 @@ function interactionsApp() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.webmentions = merged;
|
this.webmentions = merged;
|
||||||
this.hasMore = (wmData.children || []).length === this.perPage;
|
if (!silent) this.currentPage = 1;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.error = `Failed to load webmentions: ${err.message}`;
|
this.error = `Failed to load webmentions: ${err.message}`;
|
||||||
console.error('[Interactions]', err);
|
console.error('[Interactions]', err);
|
||||||
@@ -426,22 +495,18 @@ function interactionsApp() {
|
|||||||
detectPlatform(item) {
|
detectPlatform(item) {
|
||||||
const source = item['wm-source'] || '';
|
const source = item['wm-source'] || '';
|
||||||
const authorUrl = item.author?.url || '';
|
const authorUrl = item.author?.url || '';
|
||||||
// Bridgy source URLs: brid.gy/{action}/{platform}/...
|
|
||||||
if (source.includes('brid.gy/') && source.includes('/mastodon/')) return 'mastodon';
|
if (source.includes('brid.gy/') && source.includes('/mastodon/')) return 'mastodon';
|
||||||
if (source.includes('brid.gy/') && source.includes('/bluesky/')) return 'bluesky';
|
if (source.includes('brid.gy/') && source.includes('/bluesky/')) return 'bluesky';
|
||||||
// Author URL heuristics
|
|
||||||
if (authorUrl.includes('bsky.app')) return 'bluesky';
|
if (authorUrl.includes('bsky.app')) return 'bluesky';
|
||||||
if (authorUrl.includes('mstdn') || authorUrl.includes('mastodon') || authorUrl.includes('social.')) return 'mastodon';
|
if (authorUrl.includes('mstdn') || authorUrl.includes('mastodon') || authorUrl.includes('social.')) return 'mastodon';
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
mergeAndDeduplicate(wmItems, convItems) {
|
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 convUrls = new Set(convItems.map(c => c.url).filter(Boolean));
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
// Add all conversations items first (they have richer metadata)
|
|
||||||
for (const item of convItems) {
|
for (const item of convItems) {
|
||||||
const key = item['wm-id'] || item.url;
|
const key = item['wm-id'] || item.url;
|
||||||
if (key && !seen.has(key)) {
|
if (key && !seen.has(key)) {
|
||||||
@@ -450,16 +515,11 @@ function interactionsApp() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add webmention-io items that aren't duplicated by conversations
|
|
||||||
for (const item of wmItems) {
|
for (const item of wmItems) {
|
||||||
const wmKey = item['wm-id'];
|
const wmKey = item['wm-id'];
|
||||||
if (seen.has(wmKey)) continue;
|
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;
|
if (item.url && convUrls.has(item.url)) continue;
|
||||||
|
|
||||||
// Infer platform from Bridgy source URL or author URL
|
|
||||||
if (!item.platform) {
|
if (!item.platform) {
|
||||||
const detected = this.detectPlatform(item);
|
const detected = this.detectPlatform(item);
|
||||||
if (detected) item.platform = detected;
|
if (detected) item.platform = detected;
|
||||||
@@ -472,36 +532,6 @@ function interactionsApp() {
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadMore() {
|
|
||||||
this.loadingMore = true;
|
|
||||||
this.page++;
|
|
||||||
|
|
||||||
try {
|
|
||||||
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 [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: [] })),
|
|
||||||
]);
|
|
||||||
|
|
||||||
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 {
|
|
||||||
this.loadingMore = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
formatDate(dateStr) {
|
formatDate(dateStr) {
|
||||||
if (!dateStr) return '';
|
if (!dateStr) return '';
|
||||||
const date = new Date(dateStr);
|
const date = new Date(dateStr);
|
||||||
@@ -518,7 +548,6 @@ function interactionsApp() {
|
|||||||
if (!url) return '';
|
if (!url) return '';
|
||||||
try {
|
try {
|
||||||
const u = new URL(url);
|
const u = new URL(url);
|
||||||
// Return just the pathname, trimmed
|
|
||||||
let path = u.pathname;
|
let path = u.pathname;
|
||||||
if (path.length > 50) {
|
if (path.length > 50) {
|
||||||
path = path.slice(0, 47) + '...';
|
path = path.slice(0, 47) + '...';
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ pagefindIgnore: true
|
|||||||
</noscript>
|
</noscript>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
initPagefind("#search", { showSubResults: true });
|
initPagefind("#search", { showSubResults: true, showEmptyFilters: false });
|
||||||
|
|
||||||
// Support ?q= query parameter and auto-focus
|
// Support ?q= query parameter and auto-focus
|
||||||
window.addEventListener("DOMContentLoaded", () => {
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user