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 && (
|
{likes.length > 0 && (
|
||||||
<div class="facepile">
|
<div class="facepile">
|
||||||
<h3>
|
<h3>
|
||||||
{likes.length} {t(locale, likes.length === 1 ? 'webmentions.like' : 'webmentions.likes')}
|
{`${likes.length} ${t(locale, likes.length === 1 ? 'webmentions.like' : 'webmentions.likes')}`}
|
||||||
</h3>
|
</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{likes.map((m) => (
|
{likes.map((m) => (
|
||||||
|
|
@ -67,8 +67,7 @@ const hasAny = facepile.length > 0 || replies.length > 0 || mentions.length > 0;
|
||||||
{reposts.length > 0 && (
|
{reposts.length > 0 && (
|
||||||
<div class="facepile">
|
<div class="facepile">
|
||||||
<h3>
|
<h3>
|
||||||
{reposts.length}{' '}
|
{`${reposts.length} ${t(locale, reposts.length === 1 ? 'webmentions.repost' : 'webmentions.reposts')}`}
|
||||||
{t(locale, reposts.length === 1 ? 'webmentions.repost' : 'webmentions.reposts')}
|
|
||||||
</h3>
|
</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{reposts.map((m) => (
|
{reposts.map((m) => (
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
/**
|
/**
|
||||||
* Build-time Webmention fetcher via webmention.io API.
|
* Build-time Webmention fetcher via webmention.io API.
|
||||||
*
|
*
|
||||||
* Requires WEBMENTION_TOKEN in the environment (see .env.local / .env.production).
|
* Requires WEBMENTION_TOKEN in the environment. Token is read-only and
|
||||||
* Token is read-only and is issued per registered webmention.io domain.
|
* 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';
|
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';
|
const API = 'https://webmention.io/api/mentions.jf2';
|
||||||
|
|
||||||
let cache: WMResponse | null = null;
|
const perTargetCache = new Map<string, WMEntry[]>();
|
||||||
|
|
||||||
async function fetchAll(): Promise<WMResponse> {
|
async function fetchForTarget(target: string): Promise<WMEntry[]> {
|
||||||
if (cache) return cache;
|
|
||||||
const token = import.meta.env.WEBMENTION_TOKEN;
|
const token = import.meta.env.WEBMENTION_TOKEN;
|
||||||
if (!token) {
|
if (!token) {
|
||||||
console.warn('[webmentions] WEBMENTION_TOKEN is not set — skipping fetch.');
|
console.warn('[webmentions] WEBMENTION_TOKEN is not set — skipping fetch.');
|
||||||
cache = { type: 'feed', children: [] };
|
return [];
|
||||||
return cache;
|
|
||||||
}
|
}
|
||||||
const entries: WMEntry[] = [];
|
const url = new URL(API);
|
||||||
let page = 0;
|
url.searchParams.set('target', target);
|
||||||
const perPage = 100;
|
url.searchParams.set('token', token);
|
||||||
while (true) {
|
url.searchParams.set('per-page', '100');
|
||||||
const url = new URL(API);
|
const res = await fetch(url);
|
||||||
url.searchParams.set('domain', 'adrian-altner.de');
|
if (!res.ok) {
|
||||||
url.searchParams.set('token', token);
|
console.warn(`[webmentions] API ${res.status} ${res.statusText} for ${target}`);
|
||||||
url.searchParams.set('per-page', String(perPage));
|
return [];
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
cache = { type: 'feed', children: entries };
|
const json = (await res.json()) as WMResponse;
|
||||||
return cache;
|
return json.children ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return all webmentions targeting a given absolute URL.
|
* Return all webmentions targeting a given absolute URL. Tries both the
|
||||||
* Matches both with and without trailing slash; ignores hash/query.
|
* canonical URL and its trailing-slash variant, since webmention.io
|
||||||
|
* indexes targets verbatim.
|
||||||
*/
|
*/
|
||||||
export async function getMentionsFor(target: string | URL): Promise<WMEntry[]> {
|
export async function getMentionsFor(target: string | URL): Promise<WMEntry[]> {
|
||||||
const { children } = await fetchAll();
|
const canonical = typeof target === 'string' ? target : target.toString();
|
||||||
const wanted = normalize(target);
|
const withSlash = canonical.endsWith('/') ? canonical : `${canonical}/`;
|
||||||
return children.filter((m) => {
|
const withoutSlash = canonical.replace(/\/+$/, '');
|
||||||
const t = m['wm-target'];
|
|
||||||
if (!t) return false;
|
|
||||||
return normalize(t) === wanted;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalize(u: string | URL): string {
|
const cached = perTargetCache.get(canonical);
|
||||||
const url = typeof u === 'string' ? new URL(u) : new URL(u.toString());
|
if (cached) return cached;
|
||||||
url.hash = '';
|
|
||||||
url.search = '';
|
const [a, b] = await Promise.all([fetchForTarget(withSlash), fetchForTarget(withoutSlash)]);
|
||||||
const path = url.pathname.replace(/\/+$/, '');
|
const seen = new Set<number>();
|
||||||
return `${url.origin}${path}`;
|
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[]): {
|
export function groupMentions(mentions: WMEntry[]): {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue