mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
feat(link): add Open Graph fields for enhanced link sharing
- Introduced ogTitle, ogDescription, and ogImageUrl fields in the Link model for improved social media previews. - Updated the database schema to accommodate new Open Graph fields. - Modified link creation and editing forms to include inputs for Open Graph metadata. - Enhanced the GET route to serve Open Graph metadata for bots. This update allows for better customization of shared links, improving their presentation on social media platforms.
This commit is contained in:
parent
b915f15ed9
commit
e295fca187
6 changed files with 65 additions and 38 deletions
|
|
@ -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);
|
||||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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, '>')
|
||||
.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(
|
||||
`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta property="og:title" content="${l.title || l.name}">
|
||||
<meta property="og:description" content="${l.description || ''}">
|
||||
<meta property="og:image" content="${l.image || ''}">
|
||||
<meta property="og:url" content="${l.url}">
|
||||
<title>${ogTitle}</title>
|
||||
<meta property="og:title" content="${ogTitle}">
|
||||
<meta property="og:description" content="${ogDescription}">
|
||||
<meta property="og:image" content="${ogImageUrl}">
|
||||
<meta property="og:url" content="${url}">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="${l.title || l.name}">
|
||||
<meta name="twitter:description" content="${l.description || ''}">
|
||||
<meta name="twitter:image" content="${l.image || ''}">
|
||||
<meta name="twitter:title" content="${ogTitle}">
|
||||
<meta name="twitter:description" content="${ogDescription}">
|
||||
<meta name="twitter:image" content="${ogImageUrl}">
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -101,15 +101,15 @@ export function LinkEditForm({
|
|||
<TextField placeholder="https://example.com" autoComplete="off" />
|
||||
</FormField>
|
||||
|
||||
<FormField label={formatMessage(labels.title)} name="title">
|
||||
<FormField label="Title" name="ogTitle">
|
||||
<TextField autoComplete="off" />
|
||||
</FormField>
|
||||
|
||||
<FormField label={formatMessage(labels.description)} name="description">
|
||||
<FormField label="Description" name="ogDescription">
|
||||
<TextField autoComplete="off" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="OG Image URL" name="image">
|
||||
<FormField label="Image URL" name="ogImageUrl">
|
||||
<TextField autoComplete="off" />
|
||||
</FormField>
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue