Skip to content

Commit c74e42b

Browse files
web: Dynamic OpenGraph images (#8773)
Co-authored-by: Roo Code <[email protected]>
1 parent 9ac1d6e commit c74e42b

File tree

16 files changed

+430
-68
lines changed

16 files changed

+430
-68
lines changed

apps/web-roo-code/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@roo-code/evals": "workspace:^",
1818
"@roo-code/types": "workspace:^",
1919
"@tanstack/react-query": "^5.79.0",
20+
"@vercel/og": "^0.6.2",
2021
"class-variance-authority": "^0.7.1",
2122
"clsx": "^2.1.1",
2223
"embla-carousel-auto-scroll": "^8.6.0",
137 KB
Loading
144 KB
Loading
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { ImageResponse } from "next/og"
2+
import { NextRequest } from "next/server"
3+
4+
export const runtime = "edge"
5+
6+
async function fetchWithTimeout(url: string, init?: RequestInit, timeoutMs = 3000) {
7+
const controller = new AbortController()
8+
const id = setTimeout(() => controller.abort(), timeoutMs)
9+
try {
10+
return await fetch(url, { ...init, signal: controller.signal })
11+
} finally {
12+
clearTimeout(id)
13+
}
14+
}
15+
16+
async function loadGoogleFont(font: string, text: string): Promise<ArrayBuffer | null> {
17+
try {
18+
const url = `https://fonts.googleapis.com/css2?family=${font}&text=${encodeURIComponent(text)}`
19+
const cssRes = await fetchWithTimeout(url)
20+
if (!cssRes.ok) return null
21+
const css = await cssRes.text()
22+
23+
const match =
24+
css.match(/src:\s*url\(([^)]+)\)\s*format\('(?:woff2|woff|opentype|truetype)'\)/i) ||
25+
css.match(/url\(([^)]+)\)/i)
26+
27+
const fontUrl = match && match[1] ? match[1].replace(/^['"]|['"]$/g, "") : null
28+
if (!fontUrl) return null
29+
30+
const res = await fetchWithTimeout(fontUrl, undefined, 5000)
31+
if (!res.ok) return null
32+
return await res.arrayBuffer()
33+
} catch {
34+
return null
35+
}
36+
}
37+
38+
export async function GET(request: NextRequest) {
39+
const requestUrl = new URL(request.url)
40+
const { searchParams } = requestUrl
41+
42+
// Get title and description from query params
43+
const title = searchParams.get("title") || "Roo Code"
44+
const description = searchParams.get("description") || ""
45+
46+
// Combine all text that will be displayed for font loading
47+
const displayText = title + description
48+
49+
// Check if we should try to use the background image
50+
const useBackgroundImage = searchParams.get("bg") !== "false"
51+
52+
// Dynamically get the base URL from the current request
53+
// This ensures it works correctly in development, preview, and production environments
54+
const baseUrl = `${requestUrl.protocol}//${requestUrl.host}`
55+
const variant = title.length % 2 === 0 ? "a" : "b"
56+
const backgroundUrl = `${baseUrl}/og/base_${variant}.png`
57+
58+
// Preload fonts with graceful fallbacks
59+
const regularFont = await loadGoogleFont("Inter", displayText)
60+
const boldFont = await loadGoogleFont("Inter:wght@700", displayText)
61+
const fonts: { name: string; data: ArrayBuffer; style?: "normal" | "italic"; weight?: 400 | 700 }[] = []
62+
if (regularFont) {
63+
fonts.push({ name: "Inter", data: regularFont, style: "normal", weight: 400 })
64+
}
65+
if (boldFont) {
66+
fonts.push({ name: "Inter", data: boldFont, style: "normal", weight: 700 })
67+
}
68+
69+
return new ImageResponse(
70+
(
71+
<div
72+
style={{
73+
width: "100%",
74+
height: "100%",
75+
display: "flex",
76+
position: "relative",
77+
// Use gradient background as default/fallback
78+
background: "linear-gradient(135deg, #1e3a5f 0%, #0f1922 50%, #1a2332 100%)",
79+
}}>
80+
{/* Optional Background Image - only render if explicitly requested */}
81+
{useBackgroundImage && (
82+
<div
83+
style={{
84+
position: "absolute",
85+
top: 0,
86+
left: 0,
87+
width: "100%",
88+
height: "100%",
89+
display: "flex",
90+
}}>
91+
{/* eslint-disable-next-line @next/next/no-img-element */}
92+
<img
93+
src={backgroundUrl}
94+
alt=""
95+
width={1200}
96+
height={630}
97+
style={{
98+
width: "100%",
99+
height: "100%",
100+
objectFit: "cover",
101+
}}
102+
/>
103+
</div>
104+
)}
105+
106+
{/* Text Content */}
107+
<div
108+
style={{
109+
position: "absolute",
110+
display: "flex",
111+
flexDirection: "column",
112+
justifyContent: "flex-end",
113+
top: "220px",
114+
left: "80px",
115+
right: "80px",
116+
bottom: "80px",
117+
}}>
118+
{/* Main Title */}
119+
<h1
120+
style={{
121+
fontSize: 70,
122+
fontWeight: 700,
123+
fontFamily: "Inter, Helvetica Neue, Helvetica, sans-serif",
124+
color: "white",
125+
lineHeight: 1.2,
126+
margin: 0,
127+
maxHeight: "2.4em",
128+
overflow: "hidden",
129+
}}>
130+
{title}
131+
</h1>
132+
133+
{/* Secondary Description */}
134+
{description && (
135+
<h2
136+
style={{
137+
fontSize: 70,
138+
fontWeight: 400,
139+
fontFamily: "Inter, Helvetica Neue, Helvetica, Arial, sans-serif",
140+
color: "rgba(255, 255, 255, 0.9)",
141+
lineHeight: 1.2,
142+
margin: 0,
143+
maxHeight: "2.4em",
144+
overflow: "hidden",
145+
}}>
146+
{description}
147+
</h2>
148+
)}
149+
</div>
150+
</div>
151+
),
152+
{
153+
width: 1200,
154+
height: 630,
155+
fonts: fonts.length ? fonts : undefined,
156+
// Cache for 7 days in production, 3 seconds in development
157+
headers: {
158+
"Cache-Control":
159+
process.env.NODE_ENV === "production"
160+
? "public, max-age=604800, s-maxage=604800, stale-while-revalidate=86400"
161+
: "public, max-age=3, s-maxage=3",
162+
},
163+
},
164+
)
165+
}

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ import type { Metadata } from "next"
1616
import { Button } from "@/components/ui"
1717
import { AnimatedBackground } from "@/components/homepage"
1818
import { SEO } from "@/lib/seo"
19+
import { ogImageUrl } from "@/lib/og"
1920
import { EXTERNAL_LINKS } from "@/lib/constants"
2021
import Image from "next/image"
2122

2223
const TITLE = "Roo Code Cloud"
2324
const DESCRIPTION =
2425
"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."
26+
const OG_DESCRIPTION = "Go way beyond the IDE"
2527
const PATH = "/cloud"
26-
const OG_IMAGE = SEO.ogImage
2728

2829
export const metadata: Metadata = {
2930
title: TITLE,
@@ -38,10 +39,10 @@ export const metadata: Metadata = {
3839
siteName: SEO.name,
3940
images: [
4041
{
41-
url: OG_IMAGE.url,
42-
width: OG_IMAGE.width,
43-
height: OG_IMAGE.height,
44-
alt: OG_IMAGE.alt,
42+
url: ogImageUrl(TITLE, OG_DESCRIPTION),
43+
width: 1200,
44+
height: 630,
45+
alt: TITLE,
4546
},
4647
],
4748
locale: SEO.locale,
@@ -51,7 +52,7 @@ export const metadata: Metadata = {
5152
card: SEO.twitterCard,
5253
title: TITLE,
5354
description: DESCRIPTION,
54-
images: [OG_IMAGE.url],
55+
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
5556
},
5657
keywords: [...SEO.keywords, "cloud", "subscription", "cloud agents", "AI cloud development"],
5758
}

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import { ContactForm } from "@/components/enterprise/contact-form"
77
import { EXTERNAL_LINKS } from "@/lib/constants"
88
import type { Metadata } from "next"
99
import { SEO } from "@/lib/seo"
10+
import { ogImageUrl } from "@/lib/og"
1011

11-
const TITLE = "Enterprise Solution"
12+
const TITLE = "Roo Code Cloud Enterprise"
1213
const DESCRIPTION =
1314
"The control-plane for AI-powered software development. Gain visibility, governance, and control over your AI coding initiatives."
15+
const OG_DESCRIPTION = "The control-plane for AI-powered software development"
1416
const PATH = "/enterprise"
15-
const OG_IMAGE = SEO.ogImage
1617

1718
export const metadata: Metadata = {
1819
title: TITLE,
@@ -27,10 +28,10 @@ export const metadata: Metadata = {
2728
siteName: SEO.name,
2829
images: [
2930
{
30-
url: OG_IMAGE.url,
31-
width: OG_IMAGE.width,
32-
height: OG_IMAGE.height,
33-
alt: OG_IMAGE.alt,
31+
url: ogImageUrl(TITLE, OG_DESCRIPTION),
32+
width: 1200,
33+
height: 630,
34+
alt: TITLE,
3435
},
3536
],
3637
locale: SEO.locale,
@@ -40,7 +41,7 @@ export const metadata: Metadata = {
4041
card: SEO.twitterCard,
4142
title: TITLE,
4243
description: DESCRIPTION,
43-
images: [OG_IMAGE.url],
44+
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
4445
},
4546
keywords: [
4647
...SEO.keywords,

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

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Metadata } from "next"
22

33
import { getEvalRuns } from "@/actions/evals"
44
import { SEO } from "@/lib/seo"
5+
import { ogImageUrl } from "@/lib/og"
56

67
import { Evals } from "./evals"
78

@@ -10,13 +11,8 @@ export const dynamic = "force-dynamic"
1011

1112
const TITLE = "Evals"
1213
const DESCRIPTION = "Explore quantitative evals of LLM coding skills across tasks and providers."
14+
const OG_DESCRIPTION = "Quantitative evals of LLM coding skills"
1315
const PATH = "/evals"
14-
const IMAGE = {
15-
url: "https://i.imgur.com/ijP7aZm.png",
16-
width: 1954,
17-
height: 1088,
18-
alt: "Roo Code Evals – LLM coding benchmarks",
19-
}
2016

2117
export const metadata: Metadata = {
2218
title: TITLE,
@@ -29,15 +25,22 @@ export const metadata: Metadata = {
2925
description: DESCRIPTION,
3026
url: `${SEO.url}${PATH}`,
3127
siteName: SEO.name,
32-
images: [IMAGE],
28+
images: [
29+
{
30+
url: ogImageUrl(TITLE, OG_DESCRIPTION),
31+
width: 1200,
32+
height: 630,
33+
alt: TITLE,
34+
},
35+
],
3336
locale: SEO.locale,
3437
type: "website",
3538
},
3639
twitter: {
3740
card: SEO.twitterCard,
3841
title: TITLE,
3942
description: DESCRIPTION,
40-
images: [IMAGE.url],
43+
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
4144
},
4245
keywords: [...SEO.keywords, "benchmarks", "LLM evals", "coding evaluations", "model comparison"],
4346
}

apps/web-roo-code/src/app/layout.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from "react"
22
import type { Metadata } from "next"
33
import { Inter } from "next/font/google"
44
import { SEO } from "@/lib/seo"
5+
import { ogImageUrl } from "@/lib/og"
56
import { CookieConsentWrapper } from "@/components/CookieConsentWrapper"
67

78
import { Providers } from "@/components/providers"
@@ -12,6 +13,9 @@ import "./globals.css"
1213

1314
const inter = Inter({ subsets: ["latin"] })
1415

16+
const OG_TITLE = "Meet Roo Code"
17+
const OG_DESCRIPTION = "The AI dev team that gets things done."
18+
1519
export const metadata: Metadata = {
1620
metadataBase: new URL(SEO.url),
1721
title: {
@@ -51,10 +55,10 @@ export const metadata: Metadata = {
5155
siteName: SEO.name,
5256
images: [
5357
{
54-
url: SEO.ogImage.url,
55-
width: SEO.ogImage.width,
56-
height: SEO.ogImage.height,
57-
alt: SEO.ogImage.alt,
58+
url: ogImageUrl(OG_TITLE, OG_DESCRIPTION),
59+
width: 1200,
60+
height: 630,
61+
alt: OG_TITLE,
5862
},
5963
],
6064
locale: SEO.locale,
@@ -64,7 +68,7 @@ export const metadata: Metadata = {
6468
card: SEO.twitterCard,
6569
title: SEO.title,
6670
description: SEO.description,
67-
images: [SEO.ogImage.url],
71+
images: [ogImageUrl(OG_TITLE, OG_DESCRIPTION)],
6872
},
6973
robots: {
7074
index: true,

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import type { Metadata } from "next"
22
import { SEO } from "@/lib/seo"
3+
import { ogImageUrl } from "@/lib/og"
34

4-
const TITLE = "Cookie Policy"
5+
const TITLE = "Our Cookie Policy"
56
const DESCRIPTION = "Learn about how Roo Code uses cookies to enhance your experience and provide our services."
7+
const OG_DESCRIPTION = ""
68
const PATH = "/legal/cookies"
7-
const OG_IMAGE = SEO.ogImage
89

910
export const metadata: Metadata = {
1011
title: TITLE,
@@ -19,10 +20,10 @@ export const metadata: Metadata = {
1920
siteName: SEO.name,
2021
images: [
2122
{
22-
url: OG_IMAGE.url,
23-
width: OG_IMAGE.width,
24-
height: OG_IMAGE.height,
25-
alt: OG_IMAGE.alt,
23+
url: ogImageUrl(TITLE, OG_DESCRIPTION),
24+
width: 1200,
25+
height: 630,
26+
alt: TITLE,
2627
},
2728
],
2829
locale: SEO.locale,
@@ -32,7 +33,7 @@ export const metadata: Metadata = {
3233
card: SEO.twitterCard,
3334
title: TITLE,
3435
description: DESCRIPTION,
35-
images: [OG_IMAGE.url],
36+
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
3637
},
3738
keywords: [...SEO.keywords, "cookies", "privacy", "tracking", "analytics"],
3839
}

0 commit comments

Comments
 (0)