External link enhancment

This commit is contained in:
Christopher
2026-01-22 15:55:57 -08:00
parent 482acf1ff6
commit 2df45e164a
2 changed files with 30 additions and 28 deletions
+28 -27
View File
@@ -42,38 +42,35 @@ const extractTextAndUrls = (value?: string | null) => {
const normalizeUrl = (value: string) => value.replace(/[)\].,!?]+$/, '');
const fetchLinkPreview = async (url: string) => {
const fetchLinkPreview = async (url: string, origin: string) => {
try {
const res = await fetch(url, {
headers: {
'User-Agent': 'SynapsisBot/1.0',
},
const previewUrl = new URL('/api/media/preview', origin);
previewUrl.searchParams.set('url', url);
const res = await fetch(previewUrl.toString(), {
headers: { 'Accept': 'application/json' },
signal: AbortSignal.timeout(4000),
});
if (!res.ok) return null;
const html = await res.text();
const getMeta = (property: string) => {
const regex = new RegExp(`<meta[^>]+(?:property|name)=["'](?:og:)?${property}["'][^>]+content=["']([^"']+)["']`, 'i');
const match = html.match(regex);
if (match) return match[1];
const regexRev = new RegExp(`<meta[^>]+content=["']([^"']+)["'][^>]+(?:property|name)=["'](?:og:)?${property}["']`, 'i');
const matchRev = html.match(regexRev);
return matchRev ? matchRev[1] : null;
};
const title = getMeta('title') || html.match(/<title>([^<]+)<\/title>/i)?.[1];
const description = getMeta('description');
const image = getMeta('image');
const data = await res.json();
return {
url,
title: title?.trim() || url,
description: description?.trim() || null,
image: image?.trim() || null,
url: data?.url || url,
title: data?.title || null,
description: data?.description || null,
image: data?.image || null,
};
} catch {
return null;
}
};
const stripFirstUrl = (text: string, url: string) => {
const idx = text.indexOf(url);
if (idx === -1) return text;
const before = text.slice(0, idx).trimEnd();
const after = text.slice(idx + url.length).trimStart();
return `${before} ${after}`.trim();
};
const parseRemoteHandle = (handle: string) => {
const clean = handle.toLowerCase().replace(/^@/, '');
const parts = clean.split('@').filter(Boolean);
@@ -136,6 +133,7 @@ export async function GET(request: Request, context: RouteContext) {
bio: sanitizeText(remoteProfile.summary),
};
const posts = [];
const origin = new URL(request.url).origin;
for (const item of outboxItems) {
const activity = item?.type === 'Create' ? item : null;
const object = activity?.object;
@@ -145,10 +143,11 @@ export async function GET(request: Request, context: RouteContext) {
const attachments = Array.isArray(object.attachment) ? object.attachment : [];
const { text, urls } = extractTextAndUrls(object.content);
const normalizedUrl = urls.length > 0 ? normalizeUrl(urls[0]) : null;
const linkPreview = normalizedUrl ? await fetchLinkPreview(normalizedUrl) : null;
const linkPreview = normalizedUrl ? await fetchLinkPreview(normalizedUrl, origin) : null;
const contentText = linkPreview && normalizedUrl ? stripFirstUrl(text, normalizedUrl) : text;
posts.push({
id: object.id || activity.id,
content: text || '',
content: contentText || '',
createdAt: object.published || activity.published || new Date().toISOString(),
likesCount: 0,
repostsCount: 0,
@@ -162,7 +161,7 @@ export async function GET(request: Request, context: RouteContext) {
altText: sanitizeText(attachment.name) || null,
})),
linkPreviewUrl: linkPreview?.url || normalizedUrl,
linkPreviewTitle: linkPreview?.title || (normalizedUrl ?? null),
linkPreviewTitle: linkPreview?.title || null,
linkPreviewDescription: linkPreview?.description || null,
linkPreviewImage: linkPreview?.image || null,
});
@@ -194,6 +193,7 @@ export async function GET(request: Request, context: RouteContext) {
bio: sanitizeText(remoteProfile.summary),
};
const posts = [];
const origin = new URL(request.url).origin;
for (const item of outboxItems) {
const activity = item?.type === 'Create' ? item : null;
const object = activity?.object;
@@ -203,10 +203,11 @@ export async function GET(request: Request, context: RouteContext) {
const attachments = Array.isArray(object.attachment) ? object.attachment : [];
const { text, urls } = extractTextAndUrls(object.content);
const normalizedUrl = urls.length > 0 ? normalizeUrl(urls[0]) : null;
const linkPreview = normalizedUrl ? await fetchLinkPreview(normalizedUrl) : null;
const linkPreview = normalizedUrl ? await fetchLinkPreview(normalizedUrl, origin) : null;
const contentText = linkPreview && normalizedUrl ? stripFirstUrl(text, normalizedUrl) : text;
posts.push({
id: object.id || activity.id,
content: text || '',
content: contentText || '',
createdAt: object.published || activity.published || new Date().toISOString(),
likesCount: 0,
repostsCount: 0,
@@ -220,7 +221,7 @@ export async function GET(request: Request, context: RouteContext) {
altText: sanitizeText(attachment.name) || null,
})),
linkPreviewUrl: linkPreview?.url || normalizedUrl,
linkPreviewTitle: linkPreview?.title || (normalizedUrl ?? null),
linkPreviewTitle: linkPreview?.title || null,
linkPreviewDescription: linkPreview?.description || null,
linkPreviewImage: linkPreview?.image || null,
});
+2 -1
View File
@@ -1357,6 +1357,7 @@ a.btn-primary:visited {
.link-preview-card {
display: block;
margin-top: 12px;
margin-bottom: 12px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow: hidden;
@@ -1543,4 +1544,4 @@ a.btn-primary:visited {
.thread-line {
margin-left: 20px;
border-left: 1px solid var(--border);
}
}