diff --git a/src/app/api/users/[handle]/posts/route.ts b/src/app/api/users/[handle]/posts/route.ts
index 50115cf..8a210ca 100644
--- a/src/app/api/users/[handle]/posts/route.ts
+++ b/src/app/api/users/[handle]/posts/route.ts
@@ -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(`]+(?:property|name)=["'](?:og:)?${property}["'][^>]+content=["']([^"']+)["']`, 'i');
- const match = html.match(regex);
- if (match) return match[1];
- const regexRev = new RegExp(`]+content=["']([^"']+)["'][^>]+(?:property|name)=["'](?:og:)?${property}["']`, 'i');
- const matchRev = html.match(regexRev);
- return matchRev ? matchRev[1] : null;
- };
- const title = getMeta('title') || html.match(/
([^<]+)<\/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,
});
diff --git a/src/app/globals.css b/src/app/globals.css
index 78707e0..80bfcb7 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -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);
-}
\ No newline at end of file
+}