diff --git a/apps/web-roo-code/package.json b/apps/web-roo-code/package.json index c387a632709..1cf8d4dd178 100644 --- a/apps/web-roo-code/package.json +++ b/apps/web-roo-code/package.json @@ -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", diff --git a/apps/web-roo-code/public/og/base_a.png b/apps/web-roo-code/public/og/base_a.png new file mode 100644 index 00000000000..134911054a2 Binary files /dev/null and b/apps/web-roo-code/public/og/base_a.png differ diff --git a/apps/web-roo-code/public/og/base_b.png b/apps/web-roo-code/public/og/base_b.png new file mode 100644 index 00000000000..4ca1375af9f Binary files /dev/null and b/apps/web-roo-code/public/og/base_b.png differ diff --git a/apps/web-roo-code/src/app/api/og/route.tsx b/apps/web-roo-code/src/app/api/og/route.tsx new file mode 100644 index 00000000000..53390a351a2 --- /dev/null +++ b/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 { + 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( + ( +
+ {/* Optional Background Image - only render if explicitly requested */} + {useBackgroundImage && ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + +
+ )} + + {/* Text Content */} +
+ {/* Main Title */} +

+ {title} +

+ + {/* Secondary Description */} + {description && ( +

+ {description} +

+ )} +
+
+ ), + { + 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", + }, + }, + ) +} diff --git a/apps/web-roo-code/src/app/cloud/page.tsx b/apps/web-roo-code/src/app/cloud/page.tsx index 7ffc716af9c..cbf7071321b 100644 --- a/apps/web-roo-code/src/app/cloud/page.tsx +++ b/apps/web-roo-code/src/app/cloud/page.tsx @@ -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"], } diff --git a/apps/web-roo-code/src/app/enterprise/page.tsx b/apps/web-roo-code/src/app/enterprise/page.tsx index 7a1d36f0688..b4581e4f3f0 100644 --- a/apps/web-roo-code/src/app/enterprise/page.tsx +++ b/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 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, diff --git a/apps/web-roo-code/src/app/evals/page.tsx b/apps/web-roo-code/src/app/evals/page.tsx index a6af30d70ed..1c7fcfd38b7 100644 --- a/apps/web-roo-code/src/app/evals/page.tsx +++ b/apps/web-roo-code/src/app/evals/page.tsx @@ -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"], } diff --git a/apps/web-roo-code/src/app/layout.tsx b/apps/web-roo-code/src/app/layout.tsx index 08105980496..5e510eb848e 100644 --- a/apps/web-roo-code/src/app/layout.tsx +++ b/apps/web-roo-code/src/app/layout.tsx @@ -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, diff --git a/apps/web-roo-code/src/app/legal/cookies/page.tsx b/apps/web-roo-code/src/app/legal/cookies/page.tsx index cb67b8672c5..c8058a34e77 100644 --- a/apps/web-roo-code/src/app/legal/cookies/page.tsx +++ b/apps/web-roo-code/src/app/legal/cookies/page.tsx @@ -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"], } diff --git a/apps/web-roo-code/src/app/legal/subprocessors/page.tsx b/apps/web-roo-code/src/app/legal/subprocessors/page.tsx index c37bb98e851..e78fa407201 100644 --- a/apps/web-roo-code/src/app/legal/subprocessors/page.tsx +++ b/apps/web-roo-code/src/app/legal/subprocessors/page.tsx @@ -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"], } diff --git a/apps/web-roo-code/src/app/pricing/page.tsx b/apps/web-roo-code/src/app/pricing/page.tsx index b80dba6bc18..9985881e1d1 100644 --- a/apps/web-roo-code/src/app/pricing/page.tsx +++ b/apps/web-roo-code/src/app/pricing/page.tsx @@ -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, diff --git a/apps/web-roo-code/src/app/privacy/page.tsx b/apps/web-roo-code/src/app/privacy/page.tsx index 6b17c4ff8df..905e6ff1a0f 100644 --- a/apps/web-roo-code/src/app/privacy/page.tsx +++ b/apps/web-roo-code/src/app/privacy/page.tsx @@ -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"], } diff --git a/apps/web-roo-code/src/app/reviewer/page.tsx b/apps/web-roo-code/src/app/reviewer/page.tsx index a7ff6a78876..5fb00e24730 100644 --- a/apps/web-roo-code/src/app/reviewer/page.tsx +++ b/apps/web-roo-code/src/app/reviewer/page.tsx @@ -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, diff --git a/apps/web-roo-code/src/app/terms/page.tsx b/apps/web-roo-code/src/app/terms/page.tsx index 5939d8f7c96..b4f88bfcfee 100644 --- a/apps/web-roo-code/src/app/terms/page.tsx +++ b/apps/web-roo-code/src/app/terms/page.tsx @@ -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"], } diff --git a/apps/web-roo-code/src/lib/og.ts b/apps/web-roo-code/src/lib/og.ts new file mode 100644 index 00000000000..e4b2605b1ec --- /dev/null +++ b/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], + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39625d1bdcc..637469dbf07 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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