From 0dee0bc6a5c4da5785dddf0e7eb36a8050fcb6b5 Mon Sep 17 00:00:00 2001 From: Urjit Chakraborty <135136842+urjitc@users.noreply.github.com> Date: Fri, 27 Feb 2026 22:06:11 -0500 Subject: [PATCH] feat: add Open Graph metadata for share links; show Shared Workspace in link previews Made-with: Cursor --- src/app/invite/link/[token]/layout.tsx | 90 ++++++++++++++++++++++++++ src/app/share-copy/[id]/layout.tsx | 41 ++++++++++-- 2 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 src/app/invite/link/[token]/layout.tsx diff --git a/src/app/invite/link/[token]/layout.tsx b/src/app/invite/link/[token]/layout.tsx new file mode 100644 index 00000000..24c17a1a --- /dev/null +++ b/src/app/invite/link/[token]/layout.tsx @@ -0,0 +1,90 @@ +import { Metadata } from "next"; +import { db } from "@/lib/db/client"; +import { workspaceShareLinks, workspaces } from "@/lib/db/schema"; +import { eq } from "drizzle-orm"; +import { seoConfig, getPageTitle, getFullImageUrl } from "@/lib/seo-config"; + +type Props = { + params: Promise<{ token: string }>; + children: React.ReactNode; +}; + +export async function generateMetadata({ params }: Props): Promise { + const { token } = await params; + + const [shareLink] = await db + .select({ + workspaceId: workspaceShareLinks.workspaceId, + expiresAt: workspaceShareLinks.expiresAt, + workspaceName: workspaces.name, + }) + .from(workspaceShareLinks) + .innerJoin(workspaces, eq(workspaces.id, workspaceShareLinks.workspaceId)) + .where(eq(workspaceShareLinks.token, token)) + .limit(1); + + if (!shareLink) { + const notFoundTitle = "Invite Link Not Found"; + const notFoundDesc = "This invite link is invalid or has been removed."; + return { + title: getPageTitle(notFoundTitle), + description: notFoundDesc, + openGraph: { + title: getPageTitle(notFoundTitle), + description: notFoundDesc, + url: `${seoConfig.siteUrl}/invite/link/${token}`, + siteName: seoConfig.siteName, + images: [{ url: getFullImageUrl(), width: 1200, height: 630, alt: notFoundTitle }], + type: "website", + }, + twitter: { card: "summary_large_image", title: getPageTitle(notFoundTitle), description: notFoundDesc }, + }; + } + + if (new Date(shareLink.expiresAt) < new Date()) { + const expiredTitle = "Invite Link Expired"; + const expiredDesc = "This workspace invite link has expired."; + return { + title: getPageTitle(expiredTitle), + description: expiredDesc, + openGraph: { + title: getPageTitle(expiredTitle), + description: expiredDesc, + url: `${seoConfig.siteUrl}/invite/link/${token}`, + siteName: seoConfig.siteName, + images: [{ url: getFullImageUrl(), width: 1200, height: 630, alt: expiredTitle }], + type: "website", + }, + twitter: { card: "summary_large_image", title: getPageTitle(expiredTitle), description: expiredDesc }, + }; + } + + const workspaceName = shareLink.workspaceName || "Workspace"; + const sharedTitle = `Shared Workspace: ${workspaceName}`; + const description = `Join "${workspaceName}" on ThinkEx. Collaborate on notes and build knowledge together.`; + const fullTitle = getPageTitle(sharedTitle); + const url = `${seoConfig.siteUrl}/invite/link/${token}`; + + return { + title: fullTitle, + description, + openGraph: { + title: fullTitle, + description, + url, + siteName: seoConfig.siteName, + images: [{ url: getFullImageUrl(), width: 1200, height: 630, alt: sharedTitle }], + type: "website", + }, + twitter: { + card: "summary_large_image", + title: fullTitle, + description, + images: [getFullImageUrl()], + }, + }; +} + +export default function InviteLinkLayout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/src/app/share-copy/[id]/layout.tsx b/src/app/share-copy/[id]/layout.tsx index cbbebad5..a83bdfb7 100644 --- a/src/app/share-copy/[id]/layout.tsx +++ b/src/app/share-copy/[id]/layout.tsx @@ -2,15 +2,14 @@ import { Metadata } from "next"; import { db, workspaces } from "@/lib/db/client"; import { eq } from "drizzle-orm"; import { loadWorkspaceState } from "@/lib/workspace/state-loader"; +import { seoConfig, getPageTitle, getFullImageUrl } from "@/lib/seo-config"; type Props = { params: Promise<{ id: string }>; children: React.ReactNode; }; -export async function generateMetadata( - { params }: Props -): Promise { +export async function generateMetadata({ params }: Props): Promise { const { id } = await params; // Fetch workspace basic info @@ -21,9 +20,20 @@ export async function generateMetadata( .limit(1); if (!workspace[0]) { + const notFoundTitle = "Workspace Not Found"; + const notFoundDesc = "The shared workspace could not be found."; return { - title: "Workspace Not Found", - description: "The shared workspace could not be found.", + title: getPageTitle(notFoundTitle), + description: notFoundDesc, + openGraph: { + title: getPageTitle(notFoundTitle), + description: notFoundDesc, + url: `${seoConfig.siteUrl}/share-copy/${id}`, + siteName: seoConfig.siteName, + images: [{ url: getFullImageUrl(), width: 1200, height: 630, alt: notFoundTitle }], + type: "website", + }, + twitter: { card: "summary_large_image", title: getPageTitle(notFoundTitle), description: notFoundDesc }, }; } @@ -31,11 +41,28 @@ export async function generateMetadata( const state = await loadWorkspaceState(id); const title = state.globalTitle || workspace[0].name || "Untitled Workspace"; + const sharedTitle = `Shared Workspace: ${title}`; const description = workspace[0].description || "View and import this shared ThinkEx workspace."; + const fullTitle = getPageTitle(sharedTitle); + const url = `${seoConfig.siteUrl}/share-copy/${id}`; return { - title: `Shared Workspace: ${title}`, - description: description, + title: fullTitle, + description, + openGraph: { + title: fullTitle, + description, + url, + siteName: seoConfig.siteName, + images: [{ url: getFullImageUrl(), width: 1200, height: 630, alt: sharedTitle }], + type: "website", + }, + twitter: { + card: "summary_large_image", + title: fullTitle, + description, + images: [getFullImageUrl()], + }, }; }