diff --git a/prisma/migrations/20260121051633_15_add_link_og_fields/migration.sql b/prisma/migrations/20260121051633_15_add_link_og_fields/migration.sql new file mode 100644 index 00000000..d485ffac --- /dev/null +++ b/prisma/migrations/20260121051633_15_add_link_og_fields/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "link" +ADD COLUMN "og_description" VARCHAR(500), +ADD COLUMN "og_image_url" VARCHAR(500), +ADD COLUMN "og_title" VARCHAR(500); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index aa11b854..b57eeef2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -277,18 +277,18 @@ model Revenue { } model Link { - id String @id() @unique() @map("link_id") @db.Uuid - name String @db.VarChar(100) - url String @db.VarChar(500) - slug String @unique() @db.VarChar(100) - title String? @db.VarChar(500) - description String? @db.VarChar(500) - image String? @db.VarChar(500) - userId String? @map("user_id") @db.Uuid - teamId String? @map("team_id") @db.Uuid - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) - deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) + id String @id() @unique() @map("link_id") @db.Uuid + name String @db.VarChar(100) + url String @db.VarChar(500) + slug String @unique() @db.VarChar(100) + ogTitle String? @map("og_title") @db.VarChar(500) + ogDescription String? @map("og_description") @db.VarChar(500) + ogImageUrl String? @map("og_image_url") @db.VarChar(500) + userId String? @map("user_id") @db.Uuid + teamId String? @map("team_id") @db.Uuid + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) + deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) user User? @relation("user", fields: [userId], references: [id]) team Team? @relation(fields: [teamId], references: [id]) diff --git a/src/app/(collect)/q/[slug]/route.ts b/src/app/(collect)/q/[slug]/route.ts index eb0587f1..f97ab7e2 100644 --- a/src/app/(collect)/q/[slug]/route.ts +++ b/src/app/(collect)/q/[slug]/route.ts @@ -7,6 +7,15 @@ import redis from '@/lib/redis'; import { notFound } from '@/lib/response'; import { findLink } from '@/queries/prisma'; +function escapeHtml(str: string): string { + return str + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>') + .replace(/'/g, '''); +} + export async function GET(request: Request, { params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; @@ -45,22 +54,28 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug /facebookexternalhit|twitterbot|linkedinbot|whatsapp|slackbot|discordbot|telegrambot|applebot|bingbot|googlebot/i.test( userAgent, ); - const l = link; - if (isBot && (l.title || l.description || l.image)) { + + if (isBot && (link.ogTitle || link.ogDescription || link.ogImageUrl)) { + const ogTitle = escapeHtml(link.ogTitle || link.name); + const ogDescription = escapeHtml(link.ogDescription || ''); + const ogImageUrl = escapeHtml(link.ogImageUrl || ''); + const url = escapeHtml(link.url); + return new Response( ` - - - - + ${ogTitle} + + + + - - - + + + diff --git a/src/app/(main)/links/LinkEditForm.tsx b/src/app/(main)/links/LinkEditForm.tsx index bd1ffa8d..b9c16996 100644 --- a/src/app/(main)/links/LinkEditForm.tsx +++ b/src/app/(main)/links/LinkEditForm.tsx @@ -101,15 +101,15 @@ export function LinkEditForm({ - + - + - + diff --git a/src/app/api/links/[linkId]/route.ts b/src/app/api/links/[linkId]/route.ts index c44817a7..3e6444df 100644 --- a/src/app/api/links/[linkId]/route.ts +++ b/src/app/api/links/[linkId]/route.ts @@ -26,10 +26,10 @@ export async function POST(request: Request, { params }: { params: Promise<{ lin const schema = z.object({ name: z.string().optional(), url: z.string().optional(), - slug: z.string().min(8).optional(), - title: z.string().max(500).optional(), - description: z.string().max(500).optional(), - image: z.string().max(500).optional(), + slug: z.string().min(4).optional(), + ogTitle: z.string().max(500).optional(), + ogDescription: z.string().max(500).optional(), + ogImageUrl: z.url().max(500).optional().or(z.literal('')), }); const { auth, body, error } = await parseRequest(request, schema); @@ -39,14 +39,21 @@ export async function POST(request: Request, { params }: { params: Promise<{ lin } const { linkId } = await params; - const { name, url, slug, title, description, image } = body; + const { name, url, slug, ogTitle, ogDescription, ogImageUrl } = body; if (!(await canUpdateLink(auth, linkId))) { return unauthorized(); } try { - const result = await updateLink(linkId, { name, url, slug, title, description, image }); + const result = await updateLink(linkId, { + name, + url, + slug, + ogTitle, + ogDescription, + ogImageUrl, + }); return Response.json(result); } catch (e: any) { diff --git a/src/app/api/links/route.ts b/src/app/api/links/route.ts index 44f21445..9d3dbd0b 100644 --- a/src/app/api/links/route.ts +++ b/src/app/api/links/route.ts @@ -29,10 +29,10 @@ export async function POST(request: Request) { const schema = z.object({ name: z.string().max(100), url: z.string().max(500), - slug: z.string().max(100), - title: z.string().max(500).optional(), - description: z.string().max(500).optional(), - image: z.string().max(500).optional(), + slug: z.string().min(4).max(100), + ogTitle: z.string().max(500).optional(), + ogDescription: z.string().max(500).optional(), + ogImageUrl: z.url().max(500).optional().or(z.literal('')), teamId: z.string().nullable().optional(), id: z.uuid().nullable().optional(), }); @@ -43,7 +43,7 @@ export async function POST(request: Request) { return error(); } - const { id, name, url, slug, title, description, image, teamId } = body; + const { id, name, url, slug, ogTitle, ogDescription, ogImageUrl, teamId } = body; if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) { return unauthorized(); @@ -54,9 +54,9 @@ export async function POST(request: Request) { name, url, slug, - title, - description, - image, + ogTitle, + ogDescription, + ogImageUrl, teamId, };