diff --git a/src/components/Webmentions.astro b/src/components/Webmentions.astro
index e6cd802..58e9efb 100644
--- a/src/components/Webmentions.astro
+++ b/src/components/Webmentions.astro
@@ -42,7 +42,7 @@ const hasAny = facepile.length > 0 || replies.length > 0 || mentions.length > 0;
{likes.length > 0 && (
- {likes.length} {t(locale, likes.length === 1 ? 'webmentions.like' : 'webmentions.likes')}
+ {`${likes.length} ${t(locale, likes.length === 1 ? 'webmentions.like' : 'webmentions.likes')}`}
{likes.map((m) => (
@@ -67,8 +67,7 @@ const hasAny = facepile.length > 0 || replies.length > 0 || mentions.length > 0;
{reposts.length > 0 && (
- {reposts.length}{' '}
- {t(locale, reposts.length === 1 ? 'webmentions.repost' : 'webmentions.reposts')}
+ {`${reposts.length} ${t(locale, reposts.length === 1 ? 'webmentions.repost' : 'webmentions.reposts')}`}
{reposts.map((m) => (
diff --git a/src/lib/webmentions.ts b/src/lib/webmentions.ts
index 3a74116..d1a7c83 100644
--- a/src/lib/webmentions.ts
+++ b/src/lib/webmentions.ts
@@ -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();
-async function fetchAll(): Promise {
- if (cache) return cache;
+async function fetchForTarget(target: string): Promise {
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 {
- 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();
+ 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[]): {