feat(links): add Open Graph metadata support for shared links#3971
feat(links): add Open Graph metadata support for shared links#3971crbon wants to merge 9 commits intoumami-software:masterfrom
Conversation
- Add custom title field for user-defined link names - Implement custom OG image URL input for social media previews - Enable Open Graph metadata customization (description, type, etc.) - Add link slug/path customization for personalized URLs - Update link creation form with new metadata fields - Extend database schema to store custom link properties - Add validation for OG image URLs and metadata inputs This allows users to fully customize their shared analytics links with custom titles, Open Graph images, and other metadata for better social media presentation and link management.
- 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.
- Added a toggle for advanced settings in the LinkEditForm to show/hide Open Graph fields (ogTitle, ogDescription, ogImageUrl). - Updated the form to initialize these fields with default values if available. - Introduced a new label for the advanced section in the messages file. This enhancement improves user experience by allowing users to manage Open Graph metadata more efficiently.
|
@crbon is attempting to deploy a commit to the umami-software Team on Vercel. A member of the Team first needs to authorize it. |
Greptile OverviewGreptile SummaryThis PR adds Open Graph metadata support to shared links, enabling better social media previews. The implementation includes database schema changes, API endpoint updates, bot detection for serving OG meta tags, and a collapsible UI for editing OG fields. Key Changes:
Issues Found:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant LinkEditForm
participant API
participant Database
participant Bot as Social Media Bot
participant RouteHandler as /q/[slug] Route
Note over User,Database: Creating/Updating Link with OG Metadata
User->>LinkEditForm: Fill in link details
User->>LinkEditForm: Expand Advanced section
User->>LinkEditForm: Enter OG title, description, image URL
User->>LinkEditForm: Submit form
LinkEditForm->>API: POST/PATCH /api/links with OG fields
API->>API: Validate slug (min 4 chars)
API->>API: Validate ogImageUrl format
API->>Database: Store link with OG metadata
Database-->>API: Return created/updated link
API-->>LinkEditForm: Success response
LinkEditForm-->>User: Show success toast
Note over Bot,RouteHandler: Social Media Crawler Access
Bot->>RouteHandler: GET /q/[slug]
RouteHandler->>Database: Find link by slug (with Redis cache)
Database-->>RouteHandler: Return link with OG fields
RouteHandler->>RouteHandler: Detect bot via isbot(userAgent)
RouteHandler->>RouteHandler: Generate HTML with OG meta tags
RouteHandler->>RouteHandler: Use ogTitle || name for title
RouteHandler->>RouteHandler: Escape HTML content
RouteHandler-->>Bot: Return HTML with OG metadata
Bot->>Bot: Parse OG tags for preview
Note over User,RouteHandler: Regular User Access
User->>RouteHandler: GET /q/[slug]
RouteHandler->>Database: Find link by slug
Database-->>RouteHandler: Return link
RouteHandler->>RouteHandler: Detect non-bot user agent
RouteHandler->>RouteHandler: Track link click event
RouteHandler-->>User: Redirect to destination URL
|
src/app/(collect)/q/[slug]/route.ts
Outdated
| <meta property="og:description" content="${ogDescription}"> | ||
| <meta property="og:image" content="${ogImageUrl}"> |
There was a problem hiding this comment.
style: Empty og:description and og:image meta tags may cause validation issues on some social platforms. Should empty OG fields be omitted entirely rather than included as empty attributes?
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/(collect)/q/[slug]/route.ts
Line: 72:73
Comment:
**style:** Empty og:description and og:image meta tags may cause validation issues on some social platforms. Should empty OG fields be omitted entirely rather than included as empty attributes?
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.…onality - Fix inconsistent icon sizes in ExternalLink component by removing hardcoded props. - Standardize icon alignment using new CSS class. - Add copy-to-clipboard functionality when clicking the link text.
- Refactored the GET route to dynamically generate Open Graph and Twitter meta tags based on available link metadata. - Added support for conditional rendering of meta tags, improving SEO and social media sharing capabilities. - Ensured that the response includes appropriate meta tags for both Open Graph and Twitter cards, enhancing link previews.
- Introduced a new metaTag function to simplify the creation of meta tags for Open Graph and Twitter. - Replaced regex-based bot detection with the isbot library for improved accuracy. - Enhanced the GET route to conditionally render meta tags based on available link metadata, improving SEO and social media sharing capabilities.
| <FormField label="Title" name="ogTitle"> | ||
| <TextField autoComplete="off" /> | ||
| </FormField> | ||
|
|
||
| <FormField label="Description" name="ogDescription"> | ||
| <TextField autoComplete="off" /> | ||
| </FormField> | ||
|
|
||
| <FormField label="Image URL" name="ogImageUrl"> | ||
| <TextField autoComplete="off" /> | ||
| </FormField> |
There was a problem hiding this comment.
Hardcoded English labels "Title", "Description", "Image URL" inconsistent with the rest of the form which uses formatMessage(labels.*) for internationalization.
| <FormField label="Title" name="ogTitle"> | |
| <TextField autoComplete="off" /> | |
| </FormField> | |
| <FormField label="Description" name="ogDescription"> | |
| <TextField autoComplete="off" /> | |
| </FormField> | |
| <FormField label="Image URL" name="ogImageUrl"> | |
| <TextField autoComplete="off" /> | |
| </FormField> | |
| <FormField label={formatMessage(labels.title)} name="ogTitle"> | |
| <TextField autoComplete="off" /> | |
| </FormField> | |
| <FormField label={formatMessage(labels.description)} name="ogDescription"> | |
| <TextField autoComplete="off" /> | |
| </FormField> | |
| <FormField label={formatMessage(labels.imageUrl)} name="ogImageUrl"> | |
| <TextField autoComplete="off" /> | |
| </FormField> |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/(main)/links/LinkEditForm.tsx
Line: 148:158
Comment:
Hardcoded English labels "Title", "Description", "Image URL" inconsistent with the rest of the form which uses `formatMessage(labels.*)` for internationalization.
```suggestion
<FormField label={formatMessage(labels.title)} name="ogTitle">
<TextField autoComplete="off" />
</FormField>
<FormField label={formatMessage(labels.description)} name="ogDescription">
<TextField autoComplete="off" />
</FormField>
<FormField label={formatMessage(labels.imageUrl)} name="ogImageUrl">
<TextField autoComplete="off" />
</FormField>
```
How can I resolve this? If you propose a fix, please make it concise.…h fields - Replaced static labels in LinkEditForm with localized messages for ogTitle, ogDescription, and ogImageUrl. - Added a new label for imageUrl in the messages file to support localization.
src/app/api/links/route.ts
Outdated
| 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('')), |
There was a problem hiding this comment.
validation will reject empty strings before checking .or(z.literal('')) - the schema should be .optional().or(z.literal('')) without .url() first, or use .url().optional().nullable() and convert empty strings to null.
| ogImageUrl: z.url().max(500).optional().or(z.literal('')), | |
| ogImageUrl: z.string().url().optional().or(z.literal('')), |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/api/links/route.ts
Line: 35:35
Comment:
validation will reject empty strings before checking `.or(z.literal(''))` - the schema should be `.optional().or(z.literal(''))` without `.url()` first, or use `.url().optional().nullable()` and convert empty strings to null.
```suggestion
ogImageUrl: z.string().url().optional().or(z.literal('')),
```
How can I resolve this? If you propose a fix, please make it concise.
src/app/api/links/[linkId]/route.ts
Outdated
| 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('')), |
There was a problem hiding this comment.
validation will reject empty strings before checking .or(z.literal('')) - the schema should be .optional().or(z.literal('')) without .url() first, or use .url().optional().nullable() and convert empty strings to null.
| ogImageUrl: z.url().max(500).optional().or(z.literal('')), | |
| ogImageUrl: z.string().url().optional().or(z.literal('')), |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/api/links/[linkId]/route.ts
Line: 32:32
Comment:
validation will reject empty strings before checking `.or(z.literal(''))` - the schema should be `.optional().or(z.literal(''))` without `.url()` first, or use `.url().optional().nullable()` and convert empty strings to null.
```suggestion
ogImageUrl: z.string().url().optional().or(z.literal('')),
```
How can I resolve this? If you propose a fix, please make it concise.| <title>${escapeHtml(ogTitle)}</title> | ||
| ${metaTag('title', ogTitle, true)} | ||
| ${metaTag('description', ogDescription, true)} | ||
| ${metaTag('og:type', 'website')} |
There was a problem hiding this comment.
hardcoded site name 'Umami' - consider using a configuration value or environment variable for customization.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/(collect)/q/[slug]/route.ts
Line: 78:78
Comment:
hardcoded site name 'Umami' - consider using a configuration value or environment variable for customization.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.- Update schema from z.url().max() to use .pipe() with length check. - Properly validates URL format while preventing oversized input.
| <Column> | ||
| <Label>{formatMessage(labels.path)}</Label> | ||
| <TextField | ||
| value={slug} | ||
| onChange={(value: string) => { | ||
| setSlug(value); | ||
| setValue('slug', value, { shouldDirty: true }); | ||
| }} | ||
| autoComplete="off" | ||
| /> | ||
| </Column> |
There was a problem hiding this comment.
Missing validation on the slug field allows users to submit empty or invalid slugs. The API expects min(4) but there's no client-side validation, potentially causing backend errors.
| <Column> | |
| <Label>{formatMessage(labels.path)}</Label> | |
| <TextField | |
| value={slug} | |
| onChange={(value: string) => { | |
| setSlug(value); | |
| setValue('slug', value, { shouldDirty: true }); | |
| }} | |
| autoComplete="off" | |
| /> | |
| </Column> | |
| <Column> | |
| <Label>{formatMessage(labels.path)}</Label> | |
| <TextField | |
| value={slug} | |
| onChange={(value: string) => { | |
| setSlug(value); | |
| setValue('slug', value, { shouldDirty: true, shouldValidate: true }); | |
| }} | |
| autoComplete="off" | |
| minLength={4} | |
| maxLength={100} | |
| /> | |
| </Column> |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/(main)/links/LinkEditForm.tsx
Line: 160:170
Comment:
Missing validation on the slug field allows users to submit empty or invalid slugs. The API expects `min(4)` but there's no client-side validation, potentially causing backend errors.
```suggestion
<Column>
<Label>{formatMessage(labels.path)}</Label>
<TextField
value={slug}
onChange={(value: string) => {
setSlug(value);
setValue('slug', value, { shouldDirty: true, shouldValidate: true });
}}
autoComplete="off"
minLength={4}
maxLength={100}
/>
</Column>
```
How can I resolve this? If you propose a fix, please make it concise.| <Form | ||
| onSubmit={handleSubmit} | ||
| error={getErrorMessage(error)} | ||
| defaultValues={{ | ||
| slug, | ||
| ...data, | ||
| ogTitle: data?.ogTitle || '', | ||
| ogDescription: data?.ogDescription || '', | ||
| ogImageUrl: data?.ogImageUrl || '', | ||
| }} |
There was a problem hiding this comment.
The slug field is now always included in form submission even when editing an existing link and the user hasn't modified it. This differs from the previous behavior where the slug was hidden and only set during creation. When updating a link, if the user doesn't change the slug, it shouldn't be included in the update payload to avoid unnecessary database writes.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/(main)/links/LinkEditForm.tsx
Line: 85:94
Comment:
The `slug` field is now always included in form submission even when editing an existing link and the user hasn't modified it. This differs from the previous behavior where the slug was hidden and only set during creation. When updating a link, if the user doesn't change the slug, it shouldn't be included in the update payload to avoid unnecessary database writes.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.| return new Response( | ||
| ` | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8"> | ||
| <title>${escapeHtml(ogTitle)}</title> | ||
| ${metaTag('title', ogTitle, true)} | ||
| ${metaTag('description', ogDescription, true)} | ||
| ${metaTag('og:type', 'website')} | ||
| ${metaTag('og:site_name', 'Umami')} | ||
| ${metaTag('og:title', ogTitle)} | ||
| ${metaTag('og:url', request.url)} | ||
| ${metaTag('og:description', ogDescription)} | ||
| ${metaTag('og:image', ogImageUrl)} | ||
| <meta name="twitter:card" content="${twitterCard}"> | ||
| ${metaTag('twitter:title', ogTitle, true)} | ||
| ${metaTag('twitter:description', ogDescription, true)} | ||
| ${metaTag('twitter:image', ogImageUrl, true)} | ||
| </head> | ||
| <body> | ||
| <p>Redirecting to ${escapeHtml(link.url)}...</p> | ||
| </body> | ||
| </html> | ||
| `, |
There was a problem hiding this comment.
Indentation in the HTML string includes leading whitespace on every line. Consider using template literal without indentation or a template engine for cleaner HTML output.
| return new Response( | |
| ` | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>${escapeHtml(ogTitle)}</title> | |
| ${metaTag('title', ogTitle, true)} | |
| ${metaTag('description', ogDescription, true)} | |
| ${metaTag('og:type', 'website')} | |
| ${metaTag('og:site_name', 'Umami')} | |
| ${metaTag('og:title', ogTitle)} | |
| ${metaTag('og:url', request.url)} | |
| ${metaTag('og:description', ogDescription)} | |
| ${metaTag('og:image', ogImageUrl)} | |
| <meta name="twitter:card" content="${twitterCard}"> | |
| ${metaTag('twitter:title', ogTitle, true)} | |
| ${metaTag('twitter:description', ogDescription, true)} | |
| ${metaTag('twitter:image', ogImageUrl, true)} | |
| </head> | |
| <body> | |
| <p>Redirecting to ${escapeHtml(link.url)}...</p> | |
| </body> | |
| </html> | |
| `, | |
| return new Response( | |
| `<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>${escapeHtml(ogTitle)}</title> | |
| ${metaTag('title', ogTitle, true)} | |
| ${metaTag('description', ogDescription, true)} | |
| ${metaTag('og:type', 'website')} | |
| ${metaTag('og:site_name', 'Umami')} | |
| ${metaTag('og:title', ogTitle)} | |
| ${metaTag('og:url', request.url)} | |
| ${metaTag('og:description', ogDescription)} | |
| ${metaTag('og:image', ogImageUrl)} | |
| <meta name="twitter:card" content="${twitterCard}"> | |
| ${metaTag('twitter:title', ogTitle, true)} | |
| ${metaTag('twitter:description', ogDescription, true)} | |
| ${metaTag('twitter:image', ogImageUrl, true)} | |
| </head> | |
| <body> | |
| <p>Redirecting to ${escapeHtml(link.url)}...</p> | |
| </body> | |
| </html>`, |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/(collect)/q/[slug]/route.ts
Line: 69:93
Comment:
Indentation in the HTML string includes leading whitespace on every line. Consider using template literal without indentation or a template engine for cleaner HTML output.
```suggestion
return new Response(
`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>${escapeHtml(ogTitle)}</title>
${metaTag('title', ogTitle, true)}
${metaTag('description', ogDescription, true)}
${metaTag('og:type', 'website')}
${metaTag('og:site_name', 'Umami')}
${metaTag('og:title', ogTitle)}
${metaTag('og:url', request.url)}
${metaTag('og:description', ogDescription)}
${metaTag('og:image', ogImageUrl)}
<meta name="twitter:card" content="${twitterCard}">
${metaTag('twitter:title', ogTitle, true)}
${metaTag('twitter:description', ogDescription, true)}
${metaTag('twitter:image', ogImageUrl, true)}
</head>
<body>
<p>Redirecting to ${escapeHtml(link.url)}...</p>
</body>
</html>`,
```
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.…ug handling - Removed unnecessary useEffect for slug initialization and replaced it with a more efficient state setup. - Updated handleSubmit to conditionally include the slug in the payload based on link creation or modification. - Enhanced form handling by utilizing watch for dynamic slug updates and improved validation messages for slug input.
| const handleSubmit = async (formData: any) => { | ||
| const { slug: formSlug, ...rest } = formData; | ||
| // Only include slug if creating new link or if it was modified | ||
| const payload = !linkId || formSlug !== data?.slug ? formData : rest; |
There was a problem hiding this comment.
The slug comparison logic is flawed when editing links. The watch('slug') may return undefined initially (before form hydration), so the condition formSlug !== data?.slug could incorrectly evaluate to true even when the slug hasn't changed, causing unnecessary slug updates.
| const handleSubmit = async (formData: any) => { | |
| const { slug: formSlug, ...rest } = formData; | |
| // Only include slug if creating new link or if it was modified | |
| const payload = !linkId || formSlug !== data?.slug ? formData : rest; | |
| const handleSubmit = async (formData: any) => { | |
| const { slug: formSlug, ...rest } = formData; | |
| // Only include slug if creating new link or if it was modified | |
| const payload = !linkId || (formSlug && formSlug !== data?.slug) ? formData : rest; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/(main)/links/LinkEditForm.tsx
Line: 48:51
Comment:
The slug comparison logic is flawed when editing links. The `watch('slug')` may return `undefined` initially (before form hydration), so the condition `formSlug !== data?.slug` could incorrectly evaluate to true even when the slug hasn't changed, causing unnecessary slug updates.
```suggestion
const handleSubmit = async (formData: any) => {
const { slug: formSlug, ...rest } = formData;
// Only include slug if creating new link or if it was modified
const payload = !linkId || (formSlug && formSlug !== data?.slug) ? formData : rest;
```
How can I resolve this? If you propose a fix, please make it concise.| <Text title={href} truncate> | ||
| <Link {...props} href={href} target="_blank"> | ||
| <span onClick={handleCopy} className={styles.link}> | ||
| {children} | ||
| </Link> | ||
| </span> |
There was a problem hiding this comment.
Breaking UX change: clicking the link text now copies to clipboard instead of navigating. This violates user expectations where clicking text that looks like a link should navigate to that link. Users must now click the small icon to navigate.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/common/ExternalLink.tsx
Line: 24:27
Comment:
Breaking UX change: clicking the link text now copies to clipboard instead of navigating. This violates user expectations where clicking text that looks like a link should navigate to that link. Users must now click the small icon to navigate.
How can I resolve this? If you propose a fix, please make it concise.| const handleCopy = () => { | ||
| navigator.clipboard.writeText(href); | ||
| toast(formatMessage(labels.copied)); |
There was a problem hiding this comment.
Missing error handling for clipboard write operation. navigator.clipboard.writeText returns a Promise that can fail (e.g., in insecure contexts or if permissions are denied).
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/common/ExternalLink.tsx
Line: 17:19
Comment:
Missing error handling for clipboard write operation. `navigator.clipboard.writeText` returns a Promise that can fail (e.g., in insecure contexts or if permissions are denied).
How can I resolve this? If you propose a fix, please make it concise.
Add customisable Open Graph fields (title, description, image URL) to shared links, allowing improved social media previews when links are shared on platforms such as Facebook, Twitter, LinkedIn, Slack, etc. Users can also customise or specify the slug.
Changes: