Browse Source

web: Dynamic OpenGraph images (#8773)

Co-authored-by: Roo Code <[email protected]>
Bruno Bergher 2 months ago
parent
commit
c74e42bde4

+ 1 - 0
apps/web-roo-code/package.json

@@ -17,6 +17,7 @@
 		"@roo-code/evals": "workspace:^",
 		"@roo-code/evals": "workspace:^",
 		"@roo-code/types": "workspace:^",
 		"@roo-code/types": "workspace:^",
 		"@tanstack/react-query": "^5.79.0",
 		"@tanstack/react-query": "^5.79.0",
+		"@vercel/og": "^0.6.2",
 		"class-variance-authority": "^0.7.1",
 		"class-variance-authority": "^0.7.1",
 		"clsx": "^2.1.1",
 		"clsx": "^2.1.1",
 		"embla-carousel-auto-scroll": "^8.6.0",
 		"embla-carousel-auto-scroll": "^8.6.0",

BIN
apps/web-roo-code/public/og/base_a.png


BIN
apps/web-roo-code/public/og/base_b.png


+ 165 - 0
apps/web-roo-code/src/app/api/og/route.tsx

@@ -0,0 +1,165 @@
+import { ImageResponse } from "next/og"
+import { NextRequest } from "next/server"
+
+export const runtime = "edge"
+
+async function fetchWithTimeout(url: string, init?: RequestInit, timeoutMs = 3000) {
+	const controller = new AbortController()
+	const id = setTimeout(() => controller.abort(), timeoutMs)
+	try {
+		return await fetch(url, { ...init, signal: controller.signal })
+	} finally {
+		clearTimeout(id)
+	}
+}
+
+async function loadGoogleFont(font: string, text: string): Promise<ArrayBuffer | null> {
+	try {
+		const url = `https://fonts.googleapis.com/css2?family=${font}&text=${encodeURIComponent(text)}`
+		const cssRes = await fetchWithTimeout(url)
+		if (!cssRes.ok) return null
+		const css = await cssRes.text()
+
+		const match =
+			css.match(/src:\s*url\(([^)]+)\)\s*format\('(?:woff2|woff|opentype|truetype)'\)/i) ||
+			css.match(/url\(([^)]+)\)/i)
+
+		const fontUrl = match && match[1] ? match[1].replace(/^['"]|['"]$/g, "") : null
+		if (!fontUrl) return null
+
+		const res = await fetchWithTimeout(fontUrl, undefined, 5000)
+		if (!res.ok) return null
+		return await res.arrayBuffer()
+	} catch {
+		return null
+	}
+}
+
+export async function GET(request: NextRequest) {
+	const requestUrl = new URL(request.url)
+	const { searchParams } = requestUrl
+
+	// Get title and description from query params
+	const title = searchParams.get("title") || "Roo Code"
+	const description = searchParams.get("description") || ""
+
+	// Combine all text that will be displayed for font loading
+	const displayText = title + description
+
+	// Check if we should try to use the background image
+	const useBackgroundImage = searchParams.get("bg") !== "false"
+
+	// Dynamically get the base URL from the current request
+	// This ensures it works correctly in development, preview, and production environments
+	const baseUrl = `${requestUrl.protocol}//${requestUrl.host}`
+	const variant = title.length % 2 === 0 ? "a" : "b"
+	const backgroundUrl = `${baseUrl}/og/base_${variant}.png`
+
+	// Preload fonts with graceful fallbacks
+	const regularFont = await loadGoogleFont("Inter", displayText)
+	const boldFont = await loadGoogleFont("Inter:wght@700", displayText)
+	const fonts: { name: string; data: ArrayBuffer; style?: "normal" | "italic"; weight?: 400 | 700 }[] = []
+	if (regularFont) {
+		fonts.push({ name: "Inter", data: regularFont, style: "normal", weight: 400 })
+	}
+	if (boldFont) {
+		fonts.push({ name: "Inter", data: boldFont, style: "normal", weight: 700 })
+	}
+
+	return new ImageResponse(
+		(
+			<div
+				style={{
+					width: "100%",
+					height: "100%",
+					display: "flex",
+					position: "relative",
+					// Use gradient background as default/fallback
+					background: "linear-gradient(135deg, #1e3a5f 0%, #0f1922 50%, #1a2332 100%)",
+				}}>
+				{/* Optional Background Image - only render if explicitly requested */}
+				{useBackgroundImage && (
+					<div
+						style={{
+							position: "absolute",
+							top: 0,
+							left: 0,
+							width: "100%",
+							height: "100%",
+							display: "flex",
+						}}>
+						{/* eslint-disable-next-line @next/next/no-img-element */}
+						<img
+							src={backgroundUrl}
+							alt=""
+							width={1200}
+							height={630}
+							style={{
+								width: "100%",
+								height: "100%",
+								objectFit: "cover",
+							}}
+						/>
+					</div>
+				)}
+
+				{/* Text Content */}
+				<div
+					style={{
+						position: "absolute",
+						display: "flex",
+						flexDirection: "column",
+						justifyContent: "flex-end",
+						top: "220px",
+						left: "80px",
+						right: "80px",
+						bottom: "80px",
+					}}>
+					{/* Main Title */}
+					<h1
+						style={{
+							fontSize: 70,
+							fontWeight: 700,
+							fontFamily: "Inter, Helvetica Neue, Helvetica, sans-serif",
+							color: "white",
+							lineHeight: 1.2,
+							margin: 0,
+							maxHeight: "2.4em",
+							overflow: "hidden",
+						}}>
+						{title}
+					</h1>
+
+					{/* Secondary Description */}
+					{description && (
+						<h2
+							style={{
+								fontSize: 70,
+								fontWeight: 400,
+								fontFamily: "Inter, Helvetica Neue, Helvetica, Arial, sans-serif",
+								color: "rgba(255, 255, 255, 0.9)",
+								lineHeight: 1.2,
+								margin: 0,
+								maxHeight: "2.4em",
+								overflow: "hidden",
+							}}>
+							{description}
+						</h2>
+					)}
+				</div>
+			</div>
+		),
+		{
+			width: 1200,
+			height: 630,
+			fonts: fonts.length ? fonts : undefined,
+			// Cache for 7 days in production, 3 seconds in development
+			headers: {
+				"Cache-Control":
+					process.env.NODE_ENV === "production"
+						? "public, max-age=604800, s-maxage=604800, stale-while-revalidate=86400"
+						: "public, max-age=3, s-maxage=3",
+			},
+		},
+	)
+}

+ 7 - 6
apps/web-roo-code/src/app/cloud/page.tsx

@@ -16,14 +16,15 @@ import type { Metadata } from "next"
 import { Button } from "@/components/ui"
 import { Button } from "@/components/ui"
 import { AnimatedBackground } from "@/components/homepage"
 import { AnimatedBackground } from "@/components/homepage"
 import { SEO } from "@/lib/seo"
 import { SEO } from "@/lib/seo"
+import { ogImageUrl } from "@/lib/og"
 import { EXTERNAL_LINKS } from "@/lib/constants"
 import { EXTERNAL_LINKS } from "@/lib/constants"
 import Image from "next/image"
 import Image from "next/image"
 
 
 const TITLE = "Roo Code Cloud"
 const TITLE = "Roo Code Cloud"
 const DESCRIPTION =
 const DESCRIPTION =
 	"Roo Code Cloud gives you and your team the tools to take AI-coding to the next level with cloud agents, remote control, and more."
 	"Roo Code Cloud gives you and your team the tools to take AI-coding to the next level with cloud agents, remote control, and more."
+const OG_DESCRIPTION = "Go way beyond the IDE"
 const PATH = "/cloud"
 const PATH = "/cloud"
-const OG_IMAGE = SEO.ogImage
 
 
 export const metadata: Metadata = {
 export const metadata: Metadata = {
 	title: TITLE,
 	title: TITLE,
@@ -38,10 +39,10 @@ export const metadata: Metadata = {
 		siteName: SEO.name,
 		siteName: SEO.name,
 		images: [
 		images: [
 			{
 			{
-				url: OG_IMAGE.url,
-				width: OG_IMAGE.width,
-				height: OG_IMAGE.height,
-				alt: OG_IMAGE.alt,
+				url: ogImageUrl(TITLE, OG_DESCRIPTION),
+				width: 1200,
+				height: 630,
+				alt: TITLE,
 			},
 			},
 		],
 		],
 		locale: SEO.locale,
 		locale: SEO.locale,
@@ -51,7 +52,7 @@ export const metadata: Metadata = {
 		card: SEO.twitterCard,
 		card: SEO.twitterCard,
 		title: TITLE,
 		title: TITLE,
 		description: DESCRIPTION,
 		description: DESCRIPTION,
-		images: [OG_IMAGE.url],
+		images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
 	},
 	},
 	keywords: [...SEO.keywords, "cloud", "subscription", "cloud agents", "AI cloud development"],
 	keywords: [...SEO.keywords, "cloud", "subscription", "cloud agents", "AI cloud development"],
 }
 }

+ 8 - 7
apps/web-roo-code/src/app/enterprise/page.tsx

@@ -7,12 +7,13 @@ import { ContactForm } from "@/components/enterprise/contact-form"
 import { EXTERNAL_LINKS } from "@/lib/constants"
 import { EXTERNAL_LINKS } from "@/lib/constants"
 import type { Metadata } from "next"
 import type { Metadata } from "next"
 import { SEO } from "@/lib/seo"
 import { SEO } from "@/lib/seo"
+import { ogImageUrl } from "@/lib/og"
 
 
-const TITLE = "Enterprise Solution"
+const TITLE = "Roo Code Cloud Enterprise"
 const DESCRIPTION =
 const DESCRIPTION =
 	"The control-plane for AI-powered software development. Gain visibility, governance, and control over your AI coding initiatives."
 	"The control-plane for AI-powered software development. Gain visibility, governance, and control over your AI coding initiatives."
+const OG_DESCRIPTION = "The control-plane for AI-powered software development"
 const PATH = "/enterprise"
 const PATH = "/enterprise"
-const OG_IMAGE = SEO.ogImage
 
 
 export const metadata: Metadata = {
 export const metadata: Metadata = {
 	title: TITLE,
 	title: TITLE,
@@ -27,10 +28,10 @@ export const metadata: Metadata = {
 		siteName: SEO.name,
 		siteName: SEO.name,
 		images: [
 		images: [
 			{
 			{
-				url: OG_IMAGE.url,
-				width: OG_IMAGE.width,
-				height: OG_IMAGE.height,
-				alt: OG_IMAGE.alt,
+				url: ogImageUrl(TITLE, OG_DESCRIPTION),
+				width: 1200,
+				height: 630,
+				alt: TITLE,
 			},
 			},
 		],
 		],
 		locale: SEO.locale,
 		locale: SEO.locale,
@@ -40,7 +41,7 @@ export const metadata: Metadata = {
 		card: SEO.twitterCard,
 		card: SEO.twitterCard,
 		title: TITLE,
 		title: TITLE,
 		description: DESCRIPTION,
 		description: DESCRIPTION,
-		images: [OG_IMAGE.url],
+		images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
 	},
 	},
 	keywords: [
 	keywords: [
 		...SEO.keywords,
 		...SEO.keywords,

+ 11 - 8
apps/web-roo-code/src/app/evals/page.tsx

@@ -2,6 +2,7 @@ import type { Metadata } from "next"
 
 
 import { getEvalRuns } from "@/actions/evals"
 import { getEvalRuns } from "@/actions/evals"
 import { SEO } from "@/lib/seo"
 import { SEO } from "@/lib/seo"
+import { ogImageUrl } from "@/lib/og"
 
 
 import { Evals } from "./evals"
 import { Evals } from "./evals"
 
 
@@ -10,13 +11,8 @@ export const dynamic = "force-dynamic"
 
 
 const TITLE = "Evals"
 const TITLE = "Evals"
 const DESCRIPTION = "Explore quantitative evals of LLM coding skills across tasks and providers."
 const DESCRIPTION = "Explore quantitative evals of LLM coding skills across tasks and providers."
+const OG_DESCRIPTION = "Quantitative evals of LLM coding skills"
 const PATH = "/evals"
 const PATH = "/evals"
-const IMAGE = {
-	url: "https://i.imgur.com/ijP7aZm.png",
-	width: 1954,
-	height: 1088,
-	alt: "Roo Code Evals – LLM coding benchmarks",
-}
 
 
 export const metadata: Metadata = {
 export const metadata: Metadata = {
 	title: TITLE,
 	title: TITLE,
@@ -29,7 +25,14 @@ export const metadata: Metadata = {
 		description: DESCRIPTION,
 		description: DESCRIPTION,
 		url: `${SEO.url}${PATH}`,
 		url: `${SEO.url}${PATH}`,
 		siteName: SEO.name,
 		siteName: SEO.name,
-		images: [IMAGE],
+		images: [
+			{
+				url: ogImageUrl(TITLE, OG_DESCRIPTION),
+				width: 1200,
+				height: 630,
+				alt: TITLE,
+			},
+		],
 		locale: SEO.locale,
 		locale: SEO.locale,
 		type: "website",
 		type: "website",
 	},
 	},
@@ -37,7 +40,7 @@ export const metadata: Metadata = {
 		card: SEO.twitterCard,
 		card: SEO.twitterCard,
 		title: TITLE,
 		title: TITLE,
 		description: DESCRIPTION,
 		description: DESCRIPTION,
-		images: [IMAGE.url],
+		images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
 	},
 	},
 	keywords: [...SEO.keywords, "benchmarks", "LLM evals", "coding evaluations", "model comparison"],
 	keywords: [...SEO.keywords, "benchmarks", "LLM evals", "coding evaluations", "model comparison"],
 }
 }

+ 9 - 5
apps/web-roo-code/src/app/layout.tsx

@@ -2,6 +2,7 @@ import React from "react"
 import type { Metadata } from "next"
 import type { Metadata } from "next"
 import { Inter } from "next/font/google"
 import { Inter } from "next/font/google"
 import { SEO } from "@/lib/seo"
 import { SEO } from "@/lib/seo"
+import { ogImageUrl } from "@/lib/og"
 import { CookieConsentWrapper } from "@/components/CookieConsentWrapper"
 import { CookieConsentWrapper } from "@/components/CookieConsentWrapper"
 
 
 import { Providers } from "@/components/providers"
 import { Providers } from "@/components/providers"
@@ -12,6 +13,9 @@ import "./globals.css"
 
 
 const inter = Inter({ subsets: ["latin"] })
 const inter = Inter({ subsets: ["latin"] })
 
 
+const OG_TITLE = "Meet Roo Code"
+const OG_DESCRIPTION = "The AI dev team that gets things done."
+
 export const metadata: Metadata = {
 export const metadata: Metadata = {
 	metadataBase: new URL(SEO.url),
 	metadataBase: new URL(SEO.url),
 	title: {
 	title: {
@@ -51,10 +55,10 @@ export const metadata: Metadata = {
 		siteName: SEO.name,
 		siteName: SEO.name,
 		images: [
 		images: [
 			{
 			{
-				url: SEO.ogImage.url,
-				width: SEO.ogImage.width,
-				height: SEO.ogImage.height,
-				alt: SEO.ogImage.alt,
+				url: ogImageUrl(OG_TITLE, OG_DESCRIPTION),
+				width: 1200,
+				height: 630,
+				alt: OG_TITLE,
 			},
 			},
 		],
 		],
 		locale: SEO.locale,
 		locale: SEO.locale,
@@ -64,7 +68,7 @@ export const metadata: Metadata = {
 		card: SEO.twitterCard,
 		card: SEO.twitterCard,
 		title: SEO.title,
 		title: SEO.title,
 		description: SEO.description,
 		description: SEO.description,
-		images: [SEO.ogImage.url],
+		images: [ogImageUrl(OG_TITLE, OG_DESCRIPTION)],
 	},
 	},
 	robots: {
 	robots: {
 		index: true,
 		index: true,

+ 8 - 7
apps/web-roo-code/src/app/legal/cookies/page.tsx

@@ -1,10 +1,11 @@
 import type { Metadata } from "next"
 import type { Metadata } from "next"
 import { SEO } from "@/lib/seo"
 import { SEO } from "@/lib/seo"
+import { ogImageUrl } from "@/lib/og"
 
 
-const TITLE = "Cookie Policy"
+const TITLE = "Our Cookie Policy"
 const DESCRIPTION = "Learn about how Roo Code uses cookies to enhance your experience and provide our services."
 const DESCRIPTION = "Learn about how Roo Code uses cookies to enhance your experience and provide our services."
+const OG_DESCRIPTION = ""
 const PATH = "/legal/cookies"
 const PATH = "/legal/cookies"
-const OG_IMAGE = SEO.ogImage
 
 
 export const metadata: Metadata = {
 export const metadata: Metadata = {
 	title: TITLE,
 	title: TITLE,
@@ -19,10 +20,10 @@ export const metadata: Metadata = {
 		siteName: SEO.name,
 		siteName: SEO.name,
 		images: [
 		images: [
 			{
 			{
-				url: OG_IMAGE.url,
-				width: OG_IMAGE.width,
-				height: OG_IMAGE.height,
-				alt: OG_IMAGE.alt,
+				url: ogImageUrl(TITLE, OG_DESCRIPTION),
+				width: 1200,
+				height: 630,
+				alt: TITLE,
 			},
 			},
 		],
 		],
 		locale: SEO.locale,
 		locale: SEO.locale,
@@ -32,7 +33,7 @@ export const metadata: Metadata = {
 		card: SEO.twitterCard,
 		card: SEO.twitterCard,
 		title: TITLE,
 		title: TITLE,
 		description: DESCRIPTION,
 		description: DESCRIPTION,
-		images: [OG_IMAGE.url],
+		images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
 	},
 	},
 	keywords: [...SEO.keywords, "cookies", "privacy", "tracking", "analytics"],
 	keywords: [...SEO.keywords, "cookies", "privacy", "tracking", "analytics"],
 }
 }

+ 7 - 6
apps/web-roo-code/src/app/legal/subprocessors/page.tsx

@@ -1,10 +1,11 @@
 import type { Metadata } from "next"
 import type { Metadata } from "next"
 import { SEO } from "@/lib/seo"
 import { SEO } from "@/lib/seo"
+import { ogImageUrl } from "@/lib/og"
 
 
 const TITLE = "Subprocessors"
 const TITLE = "Subprocessors"
 const DESCRIPTION = "List of third-party subprocessors used by Roo Code to process customer data."
 const DESCRIPTION = "List of third-party subprocessors used by Roo Code to process customer data."
+const OG_DESCRIPTION = ""
 const PATH = "/legal/subprocessors"
 const PATH = "/legal/subprocessors"
-const OG_IMAGE = SEO.ogImage
 
 
 export const metadata: Metadata = {
 export const metadata: Metadata = {
 	title: TITLE,
 	title: TITLE,
@@ -19,10 +20,10 @@ export const metadata: Metadata = {
 		siteName: SEO.name,
 		siteName: SEO.name,
 		images: [
 		images: [
 			{
 			{
-				url: OG_IMAGE.url,
-				width: OG_IMAGE.width,
-				height: OG_IMAGE.height,
-				alt: OG_IMAGE.alt,
+				url: ogImageUrl(TITLE, OG_DESCRIPTION),
+				width: 1200,
+				height: 630,
+				alt: TITLE,
 			},
 			},
 		],
 		],
 		locale: SEO.locale,
 		locale: SEO.locale,
@@ -32,7 +33,7 @@ export const metadata: Metadata = {
 		card: SEO.twitterCard,
 		card: SEO.twitterCard,
 		title: TITLE,
 		title: TITLE,
 		description: DESCRIPTION,
 		description: DESCRIPTION,
-		images: [OG_IMAGE.url],
+		images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
 	},
 	},
 	keywords: [...SEO.keywords, "subprocessors", "data processing", "GDPR", "privacy", "third-party services"],
 	keywords: [...SEO.keywords, "subprocessors", "data processing", "GDPR", "privacy", "third-party services"],
 }
 }

+ 8 - 7
apps/web-roo-code/src/app/pricing/page.tsx

@@ -6,13 +6,14 @@ import { Button } from "@/components/ui"
 import { AnimatedBackground } from "@/components/homepage"
 import { AnimatedBackground } from "@/components/homepage"
 import { ContactForm } from "@/components/enterprise/contact-form"
 import { ContactForm } from "@/components/enterprise/contact-form"
 import { SEO } from "@/lib/seo"
 import { SEO } from "@/lib/seo"
+import { ogImageUrl } from "@/lib/og"
 import { EXTERNAL_LINKS } from "@/lib/constants"
 import { EXTERNAL_LINKS } from "@/lib/constants"
 
 
-const TITLE = "Pricing - Roo Code Cloud"
+const TITLE = "Roo Code Cloud Pricing"
 const DESCRIPTION =
 const DESCRIPTION =
 	"Simple, transparent pricing for Roo Code Cloud. The VS Code extension is free forever. Choose the cloud plan that fits your needs."
 	"Simple, transparent pricing for Roo Code Cloud. The VS Code extension is free forever. Choose the cloud plan that fits your needs."
+const OG_DESCRIPTION = ""
 const PATH = "/pricing"
 const PATH = "/pricing"
-const OG_IMAGE = SEO.ogImage
 
 
 const PRICE_CREDITS = 5
 const PRICE_CREDITS = 5
 
 
@@ -29,10 +30,10 @@ export const metadata: Metadata = {
 		siteName: SEO.name,
 		siteName: SEO.name,
 		images: [
 		images: [
 			{
 			{
-				url: OG_IMAGE.url,
-				width: OG_IMAGE.width,
-				height: OG_IMAGE.height,
-				alt: OG_IMAGE.alt,
+				url: ogImageUrl(TITLE, OG_DESCRIPTION),
+				width: 1200,
+				height: 630,
+				alt: TITLE,
 			},
 			},
 		],
 		],
 		locale: SEO.locale,
 		locale: SEO.locale,
@@ -42,7 +43,7 @@ export const metadata: Metadata = {
 		card: SEO.twitterCard,
 		card: SEO.twitterCard,
 		title: TITLE,
 		title: TITLE,
 		description: DESCRIPTION,
 		description: DESCRIPTION,
-		images: [OG_IMAGE.url],
+		images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
 	},
 	},
 	keywords: [
 	keywords: [
 		...SEO.keywords,
 		...SEO.keywords,

+ 8 - 7
apps/web-roo-code/src/app/privacy/page.tsx

@@ -1,11 +1,12 @@
 import type { Metadata } from "next"
 import type { Metadata } from "next"
 import { SEO } from "@/lib/seo"
 import { SEO } from "@/lib/seo"
+import { ogImageUrl } from "@/lib/og"
 
 
-const TITLE = "Privacy Policy"
+const TITLE = "Our Privacy Policy"
 const DESCRIPTION =
 const DESCRIPTION =
 	"Privacy policy for Roo Code Cloud and marketing website. Learn how we handle your data and protect your privacy."
 	"Privacy policy for Roo Code Cloud and marketing website. Learn how we handle your data and protect your privacy."
+const OG_DESCRIPTION = ""
 const PATH = "/privacy"
 const PATH = "/privacy"
-const OG_IMAGE = SEO.ogImage
 
 
 export const metadata: Metadata = {
 export const metadata: Metadata = {
 	title: TITLE,
 	title: TITLE,
@@ -20,10 +21,10 @@ export const metadata: Metadata = {
 		siteName: SEO.name,
 		siteName: SEO.name,
 		images: [
 		images: [
 			{
 			{
-				url: OG_IMAGE.url,
-				width: OG_IMAGE.width,
-				height: OG_IMAGE.height,
-				alt: OG_IMAGE.alt,
+				url: ogImageUrl(TITLE, OG_DESCRIPTION),
+				width: 1200,
+				height: 630,
+				alt: TITLE,
 			},
 			},
 		],
 		],
 		locale: SEO.locale,
 		locale: SEO.locale,
@@ -33,7 +34,7 @@ export const metadata: Metadata = {
 		card: SEO.twitterCard,
 		card: SEO.twitterCard,
 		title: TITLE,
 		title: TITLE,
 		description: DESCRIPTION,
 		description: DESCRIPTION,
-		images: [OG_IMAGE.url],
+		images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
 	},
 	},
 	keywords: [...SEO.keywords, "privacy", "data protection", "GDPR", "security"],
 	keywords: [...SEO.keywords, "privacy", "data protection", "GDPR", "security"],
 }
 }

+ 8 - 7
apps/web-roo-code/src/app/reviewer/page.tsx

@@ -5,14 +5,15 @@ import { Button } from "@/components/ui"
 import { AnimatedBackground } from "@/components/homepage"
 import { AnimatedBackground } from "@/components/homepage"
 import { AgentCarousel } from "@/components/reviewer/agent-carousel"
 import { AgentCarousel } from "@/components/reviewer/agent-carousel"
 import { SEO } from "@/lib/seo"
 import { SEO } from "@/lib/seo"
+import { ogImageUrl } from "@/lib/og"
 import { EXTERNAL_LINKS } from "@/lib/constants"
 import { EXTERNAL_LINKS } from "@/lib/constants"
 import Image from "next/image"
 import Image from "next/image"
 
 
-const TITLE = "PR Reviewer · Roo Code Cloud"
+const TITLE = "PR Reviewer"
 const DESCRIPTION =
 const DESCRIPTION =
 	"Get comprehensive AI-powered PR reviews that save you time, not tokens. Bring your own API key and leverage advanced reasoning, repository-aware analysis, and actionable feedback to keep your PR queue moving."
 	"Get comprehensive AI-powered PR reviews that save you time, not tokens. Bring your own API key and leverage advanced reasoning, repository-aware analysis, and actionable feedback to keep your PR queue moving."
+const OG_DESCRIPTION = "AI-powered PR reviews that save you time, not tokens"
 const PATH = "/reviewer"
 const PATH = "/reviewer"
-const OG_IMAGE = SEO.ogImage
 
 
 export const metadata: Metadata = {
 export const metadata: Metadata = {
 	title: TITLE,
 	title: TITLE,
@@ -27,10 +28,10 @@ export const metadata: Metadata = {
 		siteName: SEO.name,
 		siteName: SEO.name,
 		images: [
 		images: [
 			{
 			{
-				url: OG_IMAGE.url,
-				width: OG_IMAGE.width,
-				height: OG_IMAGE.height,
-				alt: OG_IMAGE.alt,
+				url: ogImageUrl(TITLE, OG_DESCRIPTION),
+				width: 1200,
+				height: 630,
+				alt: TITLE,
 			},
 			},
 		],
 		],
 		locale: SEO.locale,
 		locale: SEO.locale,
@@ -40,7 +41,7 @@ export const metadata: Metadata = {
 		card: SEO.twitterCard,
 		card: SEO.twitterCard,
 		title: TITLE,
 		title: TITLE,
 		description: DESCRIPTION,
 		description: DESCRIPTION,
-		images: [OG_IMAGE.url],
+		images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
 	},
 	},
 	keywords: [
 	keywords: [
 		...SEO.keywords,
 		...SEO.keywords,

+ 8 - 7
apps/web-roo-code/src/app/terms/page.tsx

@@ -1,16 +1,17 @@
 import type { Metadata } from "next"
 import type { Metadata } from "next"
 import { SEO } from "@/lib/seo"
 import { SEO } from "@/lib/seo"
+import { ogImageUrl } from "@/lib/og"
 import fs from "fs"
 import fs from "fs"
 import path from "path"
 import path from "path"
 import ReactMarkdown from "react-markdown"
 import ReactMarkdown from "react-markdown"
 import remarkGfm from "remark-gfm"
 import remarkGfm from "remark-gfm"
 import rehypeRaw from "rehype-raw"
 import rehypeRaw from "rehype-raw"
 
 
-const TITLE = "Terms of Service"
+const TITLE = "Our Terms of Service"
 const DESCRIPTION =
 const DESCRIPTION =
 	"Terms of Service for Roo Code Cloud. Learn about our service terms, commercial conditions, and legal framework."
 	"Terms of Service for Roo Code Cloud. Learn about our service terms, commercial conditions, and legal framework."
+const OG_DESCRIPTION = ""
 const PATH = "/terms"
 const PATH = "/terms"
-const OG_IMAGE = SEO.ogImage
 
 
 export const metadata: Metadata = {
 export const metadata: Metadata = {
 	title: TITLE,
 	title: TITLE,
@@ -25,10 +26,10 @@ export const metadata: Metadata = {
 		siteName: SEO.name,
 		siteName: SEO.name,
 		images: [
 		images: [
 			{
 			{
-				url: OG_IMAGE.url,
-				width: OG_IMAGE.width,
-				height: OG_IMAGE.height,
-				alt: OG_IMAGE.alt,
+				url: ogImageUrl(TITLE, OG_DESCRIPTION),
+				width: 1200,
+				height: 630,
+				alt: TITLE,
 			},
 			},
 		],
 		],
 		locale: SEO.locale,
 		locale: SEO.locale,
@@ -38,7 +39,7 @@ export const metadata: Metadata = {
 		card: SEO.twitterCard,
 		card: SEO.twitterCard,
 		title: TITLE,
 		title: TITLE,
 		description: DESCRIPTION,
 		description: DESCRIPTION,
-		images: [OG_IMAGE.url],
+		images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
 	},
 	},
 	keywords: [...SEO.keywords, "terms of service", "legal", "agreement", "subscription"],
 	keywords: [...SEO.keywords, "terms of service", "legal", "agreement", "subscription"],
 }
 }

+ 57 - 0
apps/web-roo-code/src/lib/og.ts

@@ -0,0 +1,57 @@
+/**
+ * Generate a dynamic OpenGraph image URL
+ * @param title - The title to display on the OG image
+ * @param description - Optional description to display (will be truncated to ~140 chars)
+ * @returns Absolute URL to the dynamic OG image endpoint
+ */
+export function ogImageUrl(title: string, description?: string): string {
+	const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://roocode.com"
+	const params = new URLSearchParams()
+
+	params.set("title", title)
+	if (description) {
+		params.set("description", description)
+	}
+
+	return `${baseUrl}/api/og?${params.toString()}`
+}
+
+/**
+ * Generate OpenGraph metadata for a page with dynamic image
+ * @param title - The page title
+ * @param description - The page description
+ * @returns OpenGraph metadata object with dynamic image
+ */
+export function getOgMetadata(title: string, description: string) {
+	const imageUrl = ogImageUrl(title, description)
+
+	return {
+		title,
+		description,
+		images: [
+			{
+				url: imageUrl,
+				width: 1200,
+				height: 630,
+				alt: title,
+			},
+		],
+	}
+}
+
+/**
+ * Generate Twitter metadata for a page with dynamic image
+ * @param title - The page title
+ * @param description - The page description
+ * @returns Twitter metadata object with dynamic image
+ */
+export function getTwitterMetadata(title: string, description: string) {
+	const imageUrl = ogImageUrl(title, description)
+
+	return {
+		card: "summary_large_image" as const,
+		title,
+		description,
+		images: [imageUrl],
+	}
+}

+ 125 - 1
pnpm-lock.yaml

@@ -269,6 +269,9 @@ importers:
       '@tanstack/react-query':
       '@tanstack/react-query':
         specifier: ^5.79.0
         specifier: ^5.79.0
         version: 5.80.2([email protected])
         version: 5.80.2([email protected])
+      '@vercel/og':
+        specifier: ^0.6.2
+        version: 0.6.8
       class-variance-authority:
       class-variance-authority:
         specifier: ^0.7.1
         specifier: ^0.7.1
         version: 0.7.1
         version: 0.7.1
@@ -3166,6 +3169,10 @@ packages:
     peerDependencies:
     peerDependencies:
       '@redis/client': ^5.5.5
       '@redis/client': ^5.5.5
 
 
+  '@resvg/[email protected]':
+    resolution: {integrity: sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg==}
+    engines: {node: '>= 10'}
+
   '@rollup/[email protected]':
   '@rollup/[email protected]':
     resolution: {integrity: sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==}
     resolution: {integrity: sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==}
     cpu: [arm]
     cpu: [arm]
@@ -3293,6 +3300,11 @@ packages:
   '@shikijs/[email protected]':
   '@shikijs/[email protected]':
     resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
     resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
 
 
+  '@shuding/[email protected]':
+    resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==}
+    engines: {node: '>= 8.0.0'}
+    hasBin: true
+
   '@sinclair/[email protected]':
   '@sinclair/[email protected]':
     resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
     resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
 
 
@@ -4211,6 +4223,10 @@ packages:
   '@ungap/[email protected]':
   '@ungap/[email protected]':
     resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
     resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
 
 
+  '@vercel/[email protected]':
+    resolution: {integrity: sha512-e4kQK9mP8ntpo3dACWirGod/hHv4qO5JMj9a/0a2AZto7b4persj5YP7t1Er372gTtYFTYxNhMx34jRvHooglw==}
+    engines: {node: '>=16'}
+
   '@vitejs/[email protected]':
   '@vitejs/[email protected]':
     resolution: {integrity: sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==}
     resolution: {integrity: sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==}
     engines: {node: ^14.18.0 || >=16.0.0}
     engines: {node: ^14.18.0 || >=16.0.0}
@@ -4558,6 +4574,10 @@ packages:
       bare-events:
       bare-events:
         optional: true
         optional: true
 
 
+  [email protected]:
+    resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==}
+    engines: {node: '>= 0.4'}
+
   [email protected]:
   [email protected]:
     resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
     resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
 
 
@@ -4997,10 +5017,20 @@ packages:
     resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
     resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
     engines: {node: '>= 8'}
     engines: {node: '>= 8'}
 
 
+  [email protected]:
+    resolution: {integrity: sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==}
+
   [email protected]:
   [email protected]:
     resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==}
     resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==}
     engines: {node: '>=4'}
     engines: {node: '>=4'}
 
 
+  [email protected]:
+    resolution: {integrity: sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA==}
+    engines: {node: '>=16'}
+
   [email protected]:
   [email protected]:
     resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==}
     resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==}
 
 
@@ -5983,6 +6013,9 @@ packages:
   [email protected]:
   [email protected]:
     resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
     resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
 
 
+  [email protected]:
+    resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==}
+
   [email protected]:
   [email protected]:
     resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
     resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
 
 
@@ -6382,6 +6415,10 @@ packages:
     resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
     resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
     hasBin: true
     hasBin: true
 
 
+  [email protected]:
+    resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==}
+    engines: {node: '>=6'}
+
   [email protected]:
   [email protected]:
     resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
     resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
     engines: {node: '>=12.0.0'}
     engines: {node: '>=12.0.0'}
@@ -7145,6 +7182,9 @@ packages:
     resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
     resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
     engines: {node: '>=14'}
     engines: {node: '>=14'}
 
 
+  [email protected]:
+    resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==}
+
   [email protected]:
   [email protected]:
     resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
     resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
 
 
@@ -7997,6 +8037,9 @@ packages:
   [email protected]:
   [email protected]:
     resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==}
     resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==}
 
 
+  [email protected]:
+    resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
+
   [email protected]:
   [email protected]:
     resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
     resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
 
 
@@ -8004,6 +8047,9 @@ packages:
     resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
     resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
     engines: {node: '>=6'}
     engines: {node: '>=6'}
 
 
+  [email protected]:
+    resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==}
+
   [email protected]:
   [email protected]:
     resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==}
     resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==}
 
 
@@ -8735,6 +8781,10 @@ packages:
   [email protected]:
   [email protected]:
     resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==}
     resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==}
 
 
+  [email protected]:
+    resolution: {integrity: sha512-3C/laIeE6UUe9A+iQ0A48ywPVCCMKCNSTU5Os101Vhgsjd3AAxGNjyq0uAA8kulMPK5n0csn8JlxPN9riXEjLA==}
+    engines: {node: '>=16'}
+
   [email protected]:
   [email protected]:
     resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
     resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
 
 
@@ -9052,6 +9102,9 @@ packages:
     resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
     resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
     engines: {node: '>=18'}
     engines: {node: '>=18'}
 
 
+  [email protected]:
+    resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==}
+
   [email protected]:
   [email protected]:
     resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==}
     resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==}
     engines: {node: '>= 0.4'}
     engines: {node: '>= 0.4'}
@@ -9280,6 +9333,9 @@ packages:
   [email protected]:
   [email protected]:
     resolution: {integrity: sha512-/kqtlepLMptX0OgbYD9aMYbM7EFrMZCL7EoHM8Psmg2FuhXoo/bH64KqOiZGGwa6oS9TPdSEDKBnV2LuB8+5vQ==}
     resolution: {integrity: sha512-/kqtlepLMptX0OgbYD9aMYbM7EFrMZCL7EoHM8Psmg2FuhXoo/bH64KqOiZGGwa6oS9TPdSEDKBnV2LuB8+5vQ==}
 
 
+  [email protected]:
+    resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
+
   [email protected]:
   [email protected]:
     resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
     resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
 
 
@@ -9546,6 +9602,9 @@ packages:
     resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==}
     resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==}
     engines: {node: '>=18.17'}
     engines: {node: '>=18.17'}
 
 
+  [email protected]:
+    resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==}
+
   [email protected]:
   [email protected]:
     resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
     resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
     engines: {node: '>=18'}
     engines: {node: '>=18'}
@@ -10155,6 +10214,9 @@ packages:
     resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==}
     resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==}
     engines: {node: '>=18'}
     engines: {node: '>=18'}
 
 
+  [email protected]:
+    resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==}
+
   [email protected]:
   [email protected]:
     resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==}
     resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==}
     engines: {node: '>= 10'}
     engines: {node: '>= 10'}
@@ -12628,6 +12690,8 @@ snapshots:
     dependencies:
     dependencies:
       '@redis/client': 5.5.5
       '@redis/client': 5.5.5
 
 
+  '@resvg/[email protected]': {}
+
   '@rollup/[email protected]':
   '@rollup/[email protected]':
     optional: true
     optional: true
 
 
@@ -12725,6 +12789,11 @@ snapshots:
 
 
   '@shikijs/[email protected]': {}
   '@shikijs/[email protected]': {}
 
 
+  '@shuding/[email protected]':
+    dependencies:
+      fflate: 0.7.4
+      string.prototype.codepointat: 0.2.1
+
   '@sinclair/[email protected]': {}
   '@sinclair/[email protected]': {}
 
 
   '@sindresorhus/[email protected]': {}
   '@sindresorhus/[email protected]': {}
@@ -13847,6 +13916,12 @@ snapshots:
 
 
   '@ungap/[email protected]': {}
   '@ungap/[email protected]': {}
 
 
+  '@vercel/[email protected]':
+    dependencies:
+      '@resvg/resvg-wasm': 2.4.0
+      satori: 0.12.2
+      yoga-wasm-web: 0.3.3
+
   '@vitejs/[email protected]([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected]))':
   '@vitejs/[email protected]([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected]))':
     dependencies:
     dependencies:
       '@babel/core': 7.27.1
       '@babel/core': 7.27.1
@@ -13919,7 +13994,7 @@ snapshots:
       sirv: 3.0.1
       sirv: 3.0.1
       tinyglobby: 0.2.14
       tinyglobby: 0.2.14
       tinyrainbow: 2.0.0
       tinyrainbow: 2.0.0
-      vitest: 3.2.4(@types/[email protected])(@types/node@20.17.57)(@vitest/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
+      vitest: 3.2.4(@types/[email protected])(@types/node@24.2.1)(@vitest/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
 
 
   '@vitest/[email protected]':
   '@vitest/[email protected]':
     dependencies:
     dependencies:
@@ -14290,6 +14365,8 @@ snapshots:
       bare-events: 2.5.4
       bare-events: 2.5.4
     optional: true
     optional: true
 
 
+  [email protected]: {}
+
   [email protected]: {}
   [email protected]: {}
 
 
   [email protected]: {}
   [email protected]: {}
@@ -14758,8 +14835,14 @@ snapshots:
       shebang-command: 2.0.0
       shebang-command: 2.0.0
       which: 2.0.2
       which: 2.0.2
 
 
+  [email protected]: {}
+
+  [email protected]: {}
+
   [email protected]: {}
   [email protected]: {}
 
 
+  [email protected]: {}
+
   [email protected]:
   [email protected]:
     dependencies:
     dependencies:
       hyphenate-style-name: 1.1.0
       hyphenate-style-name: 1.1.0
@@ -15865,6 +15948,8 @@ snapshots:
 
 
   [email protected]: {}
   [email protected]: {}
 
 
+  [email protected]: {}
+
   [email protected]: {}
   [email protected]: {}
 
 
   [email protected]:
   [email protected]:
@@ -16381,6 +16466,8 @@ snapshots:
 
 
   [email protected]: {}
   [email protected]: {}
 
 
+  [email protected]: {}
+
   [email protected]: {}
   [email protected]: {}
 
 
   [email protected]:
   [email protected]:
@@ -17133,6 +17220,11 @@ snapshots:
 
 
   [email protected]: {}
   [email protected]: {}
 
 
+  [email protected]:
+    dependencies:
+      base64-js: 0.0.8
+      unicode-trie: 2.0.0
+
   [email protected]: {}
   [email protected]: {}
 
 
   [email protected]:
   [email protected]:
@@ -18276,12 +18368,19 @@ snapshots:
 
 
   [email protected]: {}
   [email protected]: {}
 
 
+  [email protected]: {}
+
   [email protected]: {}
   [email protected]: {}
 
 
   [email protected]:
   [email protected]:
     dependencies:
     dependencies:
       callsites: 3.1.0
       callsites: 3.1.0
 
 
+  [email protected]:
+    dependencies:
+      color-name: 1.1.4
+      hex-rgb: 4.3.0
+
   [email protected]:
   [email protected]:
     dependencies:
     dependencies:
       character-entities: 1.2.4
       character-entities: 1.2.4
@@ -19154,6 +19253,20 @@ snapshots:
     dependencies:
     dependencies:
       truncate-utf8-bytes: 1.0.2
       truncate-utf8-bytes: 1.0.2
 
 
+  [email protected]:
+    dependencies:
+      '@shuding/opentype.js': 1.4.0-beta.0
+      css-background-parser: 0.1.0
+      css-box-shadow: 1.0.0-3
+      css-gradient-parser: 0.0.16
+      css-to-react-native: 3.2.0
+      emoji-regex: 10.4.0
+      escape-html: 1.0.3
+      linebreak: 1.1.0
+      parse-css-color: 0.2.1
+      postcss-value-parser: 4.2.0
+      yoga-wasm-web: 0.3.3
+
   [email protected]: {}
   [email protected]: {}
 
 
   [email protected]:
   [email protected]:
@@ -19532,6 +19645,8 @@ snapshots:
       get-east-asian-width: 1.3.0
       get-east-asian-width: 1.3.0
       strip-ansi: 7.1.0
       strip-ansi: 7.1.0
 
 
+  [email protected]: {}
+
   [email protected]:
   [email protected]:
     dependencies:
     dependencies:
       call-bind: 1.0.8
       call-bind: 1.0.8
@@ -19804,6 +19919,8 @@ snapshots:
 
 
   [email protected]: {}
   [email protected]: {}
 
 
+  [email protected]: {}
+
   [email protected]: {}
   [email protected]: {}
 
 
   [email protected]: {}
   [email protected]: {}
@@ -20058,6 +20175,11 @@ snapshots:
 
 
   [email protected]: {}
   [email protected]: {}
 
 
+  [email protected]:
+    dependencies:
+      pako: 0.2.9
+      tiny-inflate: 1.0.3
+
   [email protected]: {}
   [email protected]: {}
 
 
   [email protected]:
   [email protected]:
@@ -20862,6 +20984,8 @@ snapshots:
 
 
   [email protected]: {}
   [email protected]: {}
 
 
+  [email protected]: {}
+
   [email protected]:
   [email protected]:
     dependencies:
     dependencies:
       archiver-utils: 3.0.4
       archiver-utils: 3.0.4