Refactor Webmentions component to improve display of likes and reposts count
All checks were successful
Deploy / deploy (push) Successful in 1m16s
All checks were successful
Deploy / deploy (push) Successful in 1m16s
This commit is contained in:
parent
c9ad64d217
commit
42521444a8
2 changed files with 41 additions and 46 deletions
|
|
@ -42,7 +42,7 @@ const hasAny = facepile.length > 0 || replies.length > 0 || mentions.length > 0;
|
|||
{likes.length > 0 && (
|
||||
<div class="facepile">
|
||||
<h3>
|
||||
{likes.length} {t(locale, likes.length === 1 ? 'webmentions.like' : 'webmentions.likes')}
|
||||
{`${likes.length} ${t(locale, likes.length === 1 ? 'webmentions.like' : 'webmentions.likes')}`}
|
||||
</h3>
|
||||
<ul>
|
||||
{likes.map((m) => (
|
||||
|
|
@ -67,8 +67,7 @@ const hasAny = facepile.length > 0 || replies.length > 0 || mentions.length > 0;
|
|||
{reposts.length > 0 && (
|
||||
<div class="facepile">
|
||||
<h3>
|
||||
{reposts.length}{' '}
|
||||
{t(locale, reposts.length === 1 ? 'webmentions.repost' : 'webmentions.reposts')}
|
||||
{`${reposts.length} ${t(locale, reposts.length === 1 ? 'webmentions.repost' : 'webmentions.reposts')}`}
|
||||
</h3>
|
||||
<ul>
|
||||
{reposts.map((m) => (
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
/**
|
||||
* Build-time Webmention fetcher via webmention.io API.
|
||||
*
|
||||
* Requires WEBMENTION_TOKEN in the environment (see .env.local / .env.production).
|
||||
* Token is read-only and is issued per registered webmention.io domain.
|
||||
* Requires WEBMENTION_TOKEN in the environment. Token is read-only and
|
||||
* issued per registered webmention.io domain.
|
||||
*
|
||||
* Fetches per target URL (not domain-wide) to avoid inconsistencies when
|
||||
* a domain is registered more than once at webmention.io.
|
||||
*/
|
||||
|
||||
export type WMProperty = 'in-reply-to' | 'like-of' | 'repost-of' | 'bookmark-of' | 'mention-of';
|
||||
|
|
@ -37,59 +40,52 @@ interface WMResponse {
|
|||
|
||||
const API = 'https://webmention.io/api/mentions.jf2';
|
||||
|
||||
let cache: WMResponse | null = null;
|
||||
const perTargetCache = new Map<string, WMEntry[]>();
|
||||
|
||||
async function fetchAll(): Promise<WMResponse> {
|
||||
if (cache) return cache;
|
||||
async function fetchForTarget(target: string): Promise<WMEntry[]> {
|
||||
const token = import.meta.env.WEBMENTION_TOKEN;
|
||||
if (!token) {
|
||||
console.warn('[webmentions] WEBMENTION_TOKEN is not set — skipping fetch.');
|
||||
cache = { type: 'feed', children: [] };
|
||||
return cache;
|
||||
return [];
|
||||
}
|
||||
const entries: WMEntry[] = [];
|
||||
let page = 0;
|
||||
const perPage = 100;
|
||||
while (true) {
|
||||
const url = new URL(API);
|
||||
url.searchParams.set('domain', 'adrian-altner.de');
|
||||
url.searchParams.set('token', token);
|
||||
url.searchParams.set('per-page', String(perPage));
|
||||
url.searchParams.set('page', String(page));
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
console.warn(`[webmentions] API ${res.status} ${res.statusText}`);
|
||||
break;
|
||||
}
|
||||
const json = (await res.json()) as WMResponse;
|
||||
entries.push(...(json.children ?? []));
|
||||
if (!json.children || json.children.length < perPage) break;
|
||||
page += 1;
|
||||
const url = new URL(API);
|
||||
url.searchParams.set('target', target);
|
||||
url.searchParams.set('token', token);
|
||||
url.searchParams.set('per-page', '100');
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
console.warn(`[webmentions] API ${res.status} ${res.statusText} for ${target}`);
|
||||
return [];
|
||||
}
|
||||
cache = { type: 'feed', children: entries };
|
||||
return cache;
|
||||
const json = (await res.json()) as WMResponse;
|
||||
return json.children ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all webmentions targeting a given absolute URL.
|
||||
* Matches both with and without trailing slash; ignores hash/query.
|
||||
* Return all webmentions targeting a given absolute URL. Tries both the
|
||||
* canonical URL and its trailing-slash variant, since webmention.io
|
||||
* indexes targets verbatim.
|
||||
*/
|
||||
export async function getMentionsFor(target: string | URL): Promise<WMEntry[]> {
|
||||
const { children } = await fetchAll();
|
||||
const wanted = normalize(target);
|
||||
return children.filter((m) => {
|
||||
const t = m['wm-target'];
|
||||
if (!t) return false;
|
||||
return normalize(t) === wanted;
|
||||
});
|
||||
}
|
||||
const canonical = typeof target === 'string' ? target : target.toString();
|
||||
const withSlash = canonical.endsWith('/') ? canonical : `${canonical}/`;
|
||||
const withoutSlash = canonical.replace(/\/+$/, '');
|
||||
|
||||
function normalize(u: string | URL): string {
|
||||
const url = typeof u === 'string' ? new URL(u) : new URL(u.toString());
|
||||
url.hash = '';
|
||||
url.search = '';
|
||||
const path = url.pathname.replace(/\/+$/, '');
|
||||
return `${url.origin}${path}`;
|
||||
const cached = perTargetCache.get(canonical);
|
||||
if (cached) return cached;
|
||||
|
||||
const [a, b] = await Promise.all([fetchForTarget(withSlash), fetchForTarget(withoutSlash)]);
|
||||
const seen = new Set<number>();
|
||||
const merged: WMEntry[] = [];
|
||||
for (const m of [...a, ...b]) {
|
||||
const id = m['wm-id'];
|
||||
if (id == null || seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
merged.push(m);
|
||||
}
|
||||
|
||||
perTargetCache.set(canonical, merged);
|
||||
return merged;
|
||||
}
|
||||
|
||||
export function groupMentions(mentions: WMEntry[]): {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue