Rich media link previews

This commit is contained in:
Christopher
2026-01-22 13:14:58 -08:00
parent 8300001e75
commit 67a97ea8c4
5 changed files with 257 additions and 6 deletions
+56
View File
@@ -0,0 +1,56 @@
import { NextRequest, NextResponse } from 'next/server';
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
let url = searchParams.get('url');
if (!url) {
return NextResponse.json({ error: 'No URL provided' }, { status: 400 });
}
// Normalize URL - handle synapse.social etc
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
}
const response = await fetch(url, {
headers: {
'User-Agent': 'SynapsisBot/1.0',
},
signal: AbortSignal.timeout(5000), // 5s timeout
});
if (!response.ok) {
throw new Error(`Failed to fetch URL: ${response.status}`);
}
const html = await response.text();
// Simple regex extraction for OG tags
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];
// Try different order
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');
return NextResponse.json({
url,
title: title?.trim() || url,
description: description?.trim() || null,
image: image?.trim() || null,
});
} catch (error) {
console.error('Link preview error:', error);
return NextResponse.json({ error: 'Failed to fetch preview' }, { status: 500 });
}
}
+11 -1
View File
@@ -20,6 +20,12 @@ const createPostSchema = z.object({
content: z.string().min(1).max(POST_MAX_LENGTH),
replyToId: z.string().uuid().optional(),
mediaIds: z.array(z.string().uuid()).max(4).optional(),
linkPreview: z.object({
url: z.string().url(),
title: z.string().optional(),
description: z.string().optional(),
image: z.string().url().optional().nullable(),
}).optional(),
});
// Create a new post
@@ -41,6 +47,10 @@ export async function POST(request: Request) {
replyToId: data.replyToId,
apId: `https://${nodeDomain}/posts/${crypto.randomUUID()}`,
apUrl: `https://${nodeDomain}/posts/${crypto.randomUUID()}`,
linkPreviewUrl: data.linkPreview?.url,
linkPreviewTitle: data.linkPreview?.title,
linkPreviewDescription: data.linkPreview?.description,
linkPreviewImage: data.linkPreview?.image,
}).returning();
let attachedMedia: typeof media.$inferSelect[] = [];
@@ -276,7 +286,7 @@ export async function GET(request: Request) {
// Get posts from people the user follows + their own posts
// For now, just return all posts (we'll add following filter later)
feedPosts = await db.query.posts.findMany({
feedPosts = await db.query.posts.findMany({
where: baseFilter,
with: {
author: true,