web: Dynamic OpenGraph images (#8773)

Co-authored-by: Roo Code <roomote@roocode.com>
This commit is contained in:
Bruno Bergher 2025-10-22 15:04:28 +01:00 committed by GitHub
parent 9ac1d6ea14
commit c74e42bde4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 430 additions and 68 deletions

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View file

@ -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",
},
},
)
}

View file

@ -16,14 +16,15 @@ import type { Metadata } from "next"
import { Button } from "@/components/ui"
import { AnimatedBackground } from "@/components/homepage"
import { SEO } from "@/lib/seo"
import { ogImageUrl } from "@/lib/og"
import { EXTERNAL_LINKS } from "@/lib/constants"
import Image from "next/image"
const TITLE = "Roo Code Cloud"
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."
const OG_DESCRIPTION = "Go way beyond the IDE"
const PATH = "/cloud"
const OG_IMAGE = SEO.ogImage
export const metadata: Metadata = {
title: TITLE,
@ -38,10 +39,10 @@ export const metadata: Metadata = {
siteName: SEO.name,
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,
@ -51,7 +52,7 @@ export const metadata: Metadata = {
card: SEO.twitterCard,
title: TITLE,
description: DESCRIPTION,
images: [OG_IMAGE.url],
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
},
keywords: [...SEO.keywords, "cloud", "subscription", "cloud agents", "AI cloud development"],
}

View file

@ -7,12 +7,13 @@ import { ContactForm } from "@/components/enterprise/contact-form"
import { EXTERNAL_LINKS } from "@/lib/constants"
import type { Metadata } from "next"
import { SEO } from "@/lib/seo"
import { ogImageUrl } from "@/lib/og"
const TITLE = "Enterprise Solution"
const TITLE = "Roo Code Cloud Enterprise"
const DESCRIPTION =
"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 OG_IMAGE = SEO.ogImage
export const metadata: Metadata = {
title: TITLE,
@ -27,10 +28,10 @@ export const metadata: Metadata = {
siteName: SEO.name,
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,
@ -40,7 +41,7 @@ export const metadata: Metadata = {
card: SEO.twitterCard,
title: TITLE,
description: DESCRIPTION,
images: [OG_IMAGE.url],
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
},
keywords: [
...SEO.keywords,

View file

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

View file

@ -2,6 +2,7 @@ import React from "react"
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import { SEO } from "@/lib/seo"
import { ogImageUrl } from "@/lib/og"
import { CookieConsentWrapper } from "@/components/CookieConsentWrapper"
import { Providers } from "@/components/providers"
@ -12,6 +13,9 @@ import "./globals.css"
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 = {
metadataBase: new URL(SEO.url),
title: {
@ -51,10 +55,10 @@ export const metadata: Metadata = {
siteName: SEO.name,
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,
@ -64,7 +68,7 @@ export const metadata: Metadata = {
card: SEO.twitterCard,
title: SEO.title,
description: SEO.description,
images: [SEO.ogImage.url],
images: [ogImageUrl(OG_TITLE, OG_DESCRIPTION)],
},
robots: {
index: true,

View file

@ -1,10 +1,11 @@
import type { Metadata } from "next"
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 OG_DESCRIPTION = ""
const PATH = "/legal/cookies"
const OG_IMAGE = SEO.ogImage
export const metadata: Metadata = {
title: TITLE,
@ -19,10 +20,10 @@ export const metadata: Metadata = {
siteName: SEO.name,
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,
@ -32,7 +33,7 @@ export const metadata: Metadata = {
card: SEO.twitterCard,
title: TITLE,
description: DESCRIPTION,
images: [OG_IMAGE.url],
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
},
keywords: [...SEO.keywords, "cookies", "privacy", "tracking", "analytics"],
}

View file

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

View file

@ -6,13 +6,14 @@ import { Button } from "@/components/ui"
import { AnimatedBackground } from "@/components/homepage"
import { ContactForm } from "@/components/enterprise/contact-form"
import { SEO } from "@/lib/seo"
import { ogImageUrl } from "@/lib/og"
import { EXTERNAL_LINKS } from "@/lib/constants"
const TITLE = "Pricing - Roo Code Cloud"
const TITLE = "Roo Code Cloud Pricing"
const DESCRIPTION =
"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 OG_IMAGE = SEO.ogImage
const PRICE_CREDITS = 5
@ -29,10 +30,10 @@ export const metadata: Metadata = {
siteName: SEO.name,
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,
@ -42,7 +43,7 @@ export const metadata: Metadata = {
card: SEO.twitterCard,
title: TITLE,
description: DESCRIPTION,
images: [OG_IMAGE.url],
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
},
keywords: [
...SEO.keywords,

View file

@ -1,11 +1,12 @@
import type { Metadata } from "next"
import { SEO } from "@/lib/seo"
import { ogImageUrl } from "@/lib/og"
const TITLE = "Privacy Policy"
const TITLE = "Our Privacy Policy"
const DESCRIPTION =
"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 OG_IMAGE = SEO.ogImage
export const metadata: Metadata = {
title: TITLE,
@ -20,10 +21,10 @@ export const metadata: Metadata = {
siteName: SEO.name,
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,
@ -33,7 +34,7 @@ export const metadata: Metadata = {
card: SEO.twitterCard,
title: TITLE,
description: DESCRIPTION,
images: [OG_IMAGE.url],
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
},
keywords: [...SEO.keywords, "privacy", "data protection", "GDPR", "security"],
}

View file

@ -5,14 +5,15 @@ import { Button } from "@/components/ui"
import { AnimatedBackground } from "@/components/homepage"
import { AgentCarousel } from "@/components/reviewer/agent-carousel"
import { SEO } from "@/lib/seo"
import { ogImageUrl } from "@/lib/og"
import { EXTERNAL_LINKS } from "@/lib/constants"
import Image from "next/image"
const TITLE = "PR Reviewer · Roo Code Cloud"
const TITLE = "PR Reviewer"
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."
const OG_DESCRIPTION = "AI-powered PR reviews that save you time, not tokens"
const PATH = "/reviewer"
const OG_IMAGE = SEO.ogImage
export const metadata: Metadata = {
title: TITLE,
@ -27,10 +28,10 @@ export const metadata: Metadata = {
siteName: SEO.name,
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,
@ -40,7 +41,7 @@ export const metadata: Metadata = {
card: SEO.twitterCard,
title: TITLE,
description: DESCRIPTION,
images: [OG_IMAGE.url],
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
},
keywords: [
...SEO.keywords,

View file

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

View file

@ -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],
}
}

126
pnpm-lock.yaml generated
View file

@ -269,6 +269,9 @@ importers:
'@tanstack/react-query':
specifier: ^5.79.0
version: 5.80.2(react@18.3.1)
'@vercel/og':
specifier: ^0.6.2
version: 0.6.8
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@ -3166,6 +3169,10 @@ packages:
peerDependencies:
'@redis/client': ^5.5.5
'@resvg/resvg-wasm@2.4.0':
resolution: {integrity: sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg==}
engines: {node: '>= 10'}
'@rollup/rollup-android-arm-eabi@4.40.2':
resolution: {integrity: sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==}
cpu: [arm]
@ -3293,6 +3300,11 @@ packages:
'@shikijs/vscode-textmate@10.0.2':
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
'@shuding/opentype.js@1.4.0-beta.0':
resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==}
engines: {node: '>= 8.0.0'}
hasBin: true
'@sinclair/typebox@0.27.8':
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
@ -4211,6 +4223,10 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
'@vercel/og@0.6.8':
resolution: {integrity: sha512-e4kQK9mP8ntpo3dACWirGod/hHv4qO5JMj9a/0a2AZto7b4persj5YP7t1Er372gTtYFTYxNhMx34jRvHooglw==}
engines: {node: '>=16'}
'@vitejs/plugin-react@4.4.1':
resolution: {integrity: sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==}
engines: {node: ^14.18.0 || >=16.0.0}
@ -4558,6 +4574,10 @@ packages:
bare-events:
optional: true
base64-js@0.0.8:
resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==}
engines: {node: '>= 0.4'}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
@ -4997,10 +5017,20 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
css-background-parser@0.1.0:
resolution: {integrity: sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==}
css-box-shadow@1.0.0-3:
resolution: {integrity: sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==}
css-color-keywords@1.0.0:
resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==}
engines: {node: '>=4'}
css-gradient-parser@0.0.16:
resolution: {integrity: sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA==}
engines: {node: '>=16'}
css-in-js-utils@3.1.0:
resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==}
@ -5983,6 +6013,9 @@ packages:
fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
fflate@0.7.4:
resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==}
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
@ -6382,6 +6415,10 @@ packages:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
hex-rgb@4.3.0:
resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==}
engines: {node: '>=6'}
highlight.js@11.11.1:
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
engines: {node: '>=12.0.0'}
@ -7145,6 +7182,9 @@ packages:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
linebreak@1.1.0:
resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==}
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
@ -7997,6 +8037,9 @@ packages:
package-manager-detector@1.3.0:
resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==}
pako@0.2.9:
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
@ -8004,6 +8047,9 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
parse-css-color@0.2.1:
resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==}
parse-entities@2.0.0:
resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==}
@ -8735,6 +8781,10 @@ packages:
sanitize-filename@1.6.3:
resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==}
satori@0.12.2:
resolution: {integrity: sha512-3C/laIeE6UUe9A+iQ0A48ywPVCCMKCNSTU5Os101Vhgsjd3AAxGNjyq0uAA8kulMPK5n0csn8JlxPN9riXEjLA==}
engines: {node: '>=16'}
sax@1.4.1:
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
@ -9052,6 +9102,9 @@ packages:
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
engines: {node: '>=18'}
string.prototype.codepointat@0.2.1:
resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==}
string.prototype.matchall@4.0.12:
resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==}
engines: {node: '>= 0.4'}
@ -9280,6 +9333,9 @@ packages:
tiktoken@1.0.21:
resolution: {integrity: sha512-/kqtlepLMptX0OgbYD9aMYbM7EFrMZCL7EoHM8Psmg2FuhXoo/bH64KqOiZGGwa6oS9TPdSEDKBnV2LuB8+5vQ==}
tiny-inflate@1.0.3:
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
@ -9546,6 +9602,9 @@ packages:
resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==}
engines: {node: '>=18.17'}
unicode-trie@2.0.0:
resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==}
unicorn-magic@0.3.0:
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
engines: {node: '>=18'}
@ -10155,6 +10214,9 @@ packages:
resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==}
engines: {node: '>=18'}
yoga-wasm-web@0.3.3:
resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==}
zip-stream@4.1.1:
resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==}
engines: {node: '>= 10'}
@ -12628,6 +12690,8 @@ snapshots:
dependencies:
'@redis/client': 5.5.5
'@resvg/resvg-wasm@2.4.0': {}
'@rollup/rollup-android-arm-eabi@4.40.2':
optional: true
@ -12725,6 +12789,11 @@ snapshots:
'@shikijs/vscode-textmate@10.0.2': {}
'@shuding/opentype.js@1.4.0-beta.0':
dependencies:
fflate: 0.7.4
string.prototype.codepointat: 0.2.1
'@sinclair/typebox@0.27.8': {}
'@sindresorhus/merge-streams@4.0.0': {}
@ -13847,6 +13916,12 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
'@vercel/og@0.6.8':
dependencies:
'@resvg/resvg-wasm': 2.4.0
satori: 0.12.2
yoga-wasm-web: 0.3.3
'@vitejs/plugin-react@4.4.1(vite@6.3.6(@types/node@20.17.57)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0))':
dependencies:
'@babel/core': 7.27.1
@ -13919,7 +13994,7 @@ snapshots:
sirv: 3.0.1
tinyglobby: 0.2.14
tinyrainbow: 2.0.0
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
'@vitest/utils@3.2.4':
dependencies:
@ -14290,6 +14365,8 @@ snapshots:
bare-events: 2.5.4
optional: true
base64-js@0.0.8: {}
base64-js@1.5.1: {}
basic-ftp@5.0.5: {}
@ -14758,8 +14835,14 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
css-background-parser@0.1.0: {}
css-box-shadow@1.0.0-3: {}
css-color-keywords@1.0.0: {}
css-gradient-parser@0.0.16: {}
css-in-js-utils@3.1.0:
dependencies:
hyphenate-style-name: 1.1.0
@ -15865,6 +15948,8 @@ snapshots:
fflate@0.4.8: {}
fflate@0.7.4: {}
fflate@0.8.2: {}
figures@6.1.0:
@ -16381,6 +16466,8 @@ snapshots:
he@1.2.0: {}
hex-rgb@4.3.0: {}
highlight.js@11.11.1: {}
hosted-git-info@4.1.0:
@ -17133,6 +17220,11 @@ snapshots:
lilconfig@3.1.3: {}
linebreak@1.1.0:
dependencies:
base64-js: 0.0.8
unicode-trie: 2.0.0
lines-and-columns@1.2.4: {}
linkify-it@5.0.0:
@ -18276,12 +18368,19 @@ snapshots:
package-manager-detector@1.3.0: {}
pako@0.2.9: {}
pako@1.0.11: {}
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
parse-css-color@0.2.1:
dependencies:
color-name: 1.1.4
hex-rgb: 4.3.0
parse-entities@2.0.0:
dependencies:
character-entities: 1.2.4
@ -19154,6 +19253,20 @@ snapshots:
dependencies:
truncate-utf8-bytes: 1.0.2
satori@0.12.2:
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
sax@1.4.1: {}
saxes@5.0.1:
@ -19532,6 +19645,8 @@ snapshots:
get-east-asian-width: 1.3.0
strip-ansi: 7.1.0
string.prototype.codepointat@0.2.1: {}
string.prototype.matchall@4.0.12:
dependencies:
call-bind: 1.0.8
@ -19804,6 +19919,8 @@ snapshots:
tiktoken@1.0.21: {}
tiny-inflate@1.0.3: {}
tiny-invariant@1.3.3: {}
tinybench@2.9.0: {}
@ -20058,6 +20175,11 @@ snapshots:
undici@6.21.3: {}
unicode-trie@2.0.0:
dependencies:
pako: 0.2.9
tiny-inflate: 1.0.3
unicorn-magic@0.3.0: {}
unified@11.0.5:
@ -20862,6 +20984,8 @@ snapshots:
yoctocolors@2.1.1: {}
yoga-wasm-web@0.3.3: {}
zip-stream@4.1.1:
dependencies:
archiver-utils: 3.0.4