Implement Webmention functionality: add Webmentions component, update deploy script, and enhance UI with social links and localization
This commit is contained in:
parent
abbf2d9a0b
commit
934a9f2338
8 changed files with 460 additions and 10 deletions
126
src/lib/webmentions.ts
Normal file
126
src/lib/webmentions.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type WMProperty = 'in-reply-to' | 'like-of' | 'repost-of' | 'bookmark-of' | 'mention-of';
|
||||
|
||||
export interface WMAuthor {
|
||||
type?: string;
|
||||
name?: string;
|
||||
url?: string;
|
||||
photo?: string;
|
||||
}
|
||||
|
||||
export interface WMEntry {
|
||||
type: 'entry';
|
||||
author?: WMAuthor;
|
||||
url: string;
|
||||
published?: string;
|
||||
'wm-received'?: string;
|
||||
'wm-id'?: number;
|
||||
'wm-source'?: string;
|
||||
'wm-target'?: string;
|
||||
'wm-property'?: WMProperty;
|
||||
'wm-private'?: boolean;
|
||||
content?: { text?: string; html?: string };
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface WMResponse {
|
||||
type: 'feed';
|
||||
name?: string;
|
||||
children: WMEntry[];
|
||||
}
|
||||
|
||||
const API = 'https://webmention.io/api/mentions.jf2';
|
||||
|
||||
let cache: WMResponse | null = null;
|
||||
|
||||
async function fetchAll(): Promise<WMResponse> {
|
||||
if (cache) return cache;
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
cache = { type: 'feed', children: entries };
|
||||
return cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all webmentions targeting a given absolute URL.
|
||||
* Matches both with and without trailing slash; ignores hash/query.
|
||||
*/
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
export function groupMentions(mentions: WMEntry[]): {
|
||||
likes: WMEntry[];
|
||||
reposts: WMEntry[];
|
||||
bookmarks: WMEntry[];
|
||||
replies: WMEntry[];
|
||||
mentions: WMEntry[];
|
||||
} {
|
||||
const likes: WMEntry[] = [];
|
||||
const reposts: WMEntry[] = [];
|
||||
const bookmarks: WMEntry[] = [];
|
||||
const replies: WMEntry[] = [];
|
||||
const others: WMEntry[] = [];
|
||||
for (const m of mentions) {
|
||||
switch (m['wm-property']) {
|
||||
case 'like-of':
|
||||
likes.push(m);
|
||||
break;
|
||||
case 'repost-of':
|
||||
reposts.push(m);
|
||||
break;
|
||||
case 'bookmark-of':
|
||||
bookmarks.push(m);
|
||||
break;
|
||||
case 'in-reply-to':
|
||||
replies.push(m);
|
||||
break;
|
||||
default:
|
||||
others.push(m);
|
||||
}
|
||||
}
|
||||
return { likes, reposts, bookmarks, replies, mentions: others };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue