Created
July 29, 2025 22:35
-
-
Save vanxh/c1b450a80e24bfb2a3e7500918f92df0 to your computer and use it in GitHub Desktop.
Tanstack start dynamic OG image creation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { api } from "@connectlabs/backend/convex/_generated/api"; | |
| import type { Id } from "@connectlabs/backend/convex/_generated/dataModel"; | |
| import { Resvg } from "@resvg/resvg-js"; | |
| import { createServerFileRoute } from "@tanstack/react-start/server"; | |
| import { ConvexHttpClient } from "convex/browser"; | |
| import satori from "satori"; | |
| import sharp from "sharp"; | |
| type AgentData = { | |
| name: string; | |
| image: string; | |
| type: string; | |
| _id: string; | |
| isPublic?: boolean; | |
| }; | |
| async function convertWebPToPNG(imageUrl: string): Promise<string | null> { | |
| try { | |
| const response = await fetch(imageUrl); | |
| if (!response.ok) { | |
| console.warn(`Image fetch failed: ${response.status} for ${imageUrl}`); | |
| return null; | |
| } | |
| const webpBuffer = await response.arrayBuffer(); | |
| const pngBuffer = await sharp(Buffer.from(webpBuffer)) | |
| .png() | |
| .resize(160, 160) | |
| .toBuffer(); | |
| return `data:image/png;base64,${pngBuffer.toString("base64")}`; | |
| } catch (error) { | |
| console.error("Failed to convert WebP to PNG:", error); | |
| return null; | |
| } | |
| } | |
| async function convertLogoToPNG(): Promise<string | null> { | |
| try { | |
| const logoUrl = "https://connectlabs.ai/logo.png"; | |
| const response = await fetch(logoUrl); | |
| if (!response.ok) return null; | |
| const logoBuffer = await response.arrayBuffer(); | |
| const pngBuffer = await sharp(Buffer.from(logoBuffer)) | |
| .png() | |
| .resize(40, 40) | |
| .toBuffer(); | |
| return `data:image/png;base64,${pngBuffer.toString("base64")}`; | |
| } catch (error) { | |
| console.error("Failed to convert logo:", error); | |
| return null; | |
| } | |
| } | |
| async function getFonts() { | |
| try { | |
| const fontUrl = | |
| "https://cdn.jsdelivr.net/fontsource/fonts/lexend@latest/latin-400-normal.ttf"; | |
| const fontDataResponse = await fetch(fontUrl); | |
| if (!fontDataResponse.ok) { | |
| console.warn( | |
| `Lexend font fetch failed: ${fontDataResponse.status}, trying fallback`, | |
| ); | |
| throw new Error("Primary font failed"); | |
| } | |
| const fontData = await fontDataResponse.arrayBuffer(); | |
| return [ | |
| { | |
| name: "Lexend", | |
| data: fontData, | |
| weight: 400 as const, | |
| style: "normal" as const, | |
| }, | |
| ]; | |
| } catch (error) { | |
| console.error("Lexend font loading failed:", error); | |
| try { | |
| const fallbackUrl = | |
| "https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-400-normal.ttf"; | |
| const fallbackResponse = await fetch(fallbackUrl); | |
| const fallbackData = await fallbackResponse.arrayBuffer(); | |
| return [ | |
| { | |
| name: "Inter", | |
| data: fallbackData, | |
| weight: 400 as const, | |
| style: "normal" as const, | |
| }, | |
| ]; | |
| } catch (fallbackError) { | |
| console.error("All font loading failed:", fallbackError); | |
| throw new Error("No fonts could be loaded"); | |
| } | |
| } | |
| } | |
| async function generateOGImage(agent: AgentData) { | |
| try { | |
| let processedImageSrc = agent.image; | |
| if (agent.image.startsWith("/memojis/")) { | |
| const imageUrl = `https://connectlabs.ai${agent.image}`; | |
| const convertedImage = await convertWebPToPNG(imageUrl); | |
| if (convertedImage) { | |
| processedImageSrc = convertedImage; | |
| } | |
| } | |
| const logoSrc = await convertLogoToPNG(); | |
| const svg = await satori( | |
| <div | |
| style={{ | |
| height: "100%", | |
| width: "100%", | |
| display: "flex", | |
| flexDirection: "column", | |
| alignItems: "center", | |
| justifyContent: "center", | |
| backgroundColor: "#ffffff", | |
| backgroundImage: "radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.08) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.08) 0%, transparent 50%)", | |
| color: "#000000", | |
| fontFamily: "Lexend", | |
| position: "relative", | |
| }} | |
| > | |
| <div | |
| style={{ | |
| position: "absolute", | |
| top: 60, | |
| right: 60, | |
| padding: "12px 20px", | |
| backgroundColor: "rgba(255, 255, 255, 0.9)", | |
| borderRadius: "25px", | |
| border: "1px solid rgba(0, 0, 0, 0.08)", | |
| boxShadow: "0 4px 20px rgba(0, 0, 0, 0.08)", | |
| fontSize: 16, | |
| fontWeight: 500, | |
| color: "#666666", | |
| }} | |
| > | |
| AI Voice Assistant | |
| </div> | |
| {processedImageSrc ? ( | |
| <div | |
| style={{ | |
| width: 180, | |
| height: 180, | |
| borderRadius: "50%", | |
| overflow: "hidden", | |
| marginBottom: 50, | |
| border: "6px solid rgba(255, 255, 255, 0.8)", | |
| display: "flex", | |
| alignItems: "center", | |
| justifyContent: "center", | |
| background: "#ffffff", | |
| boxShadow: "0 8px 32px rgba(0, 0, 0, 0.12)", | |
| }} | |
| > | |
| <img | |
| src={processedImageSrc} | |
| alt={agent.name} | |
| width={180} | |
| height={180} | |
| style={{ | |
| width: "100%", | |
| height: "100%", | |
| objectFit: "cover", | |
| }} | |
| /> | |
| </div> | |
| ) : ( | |
| <div | |
| style={{ | |
| width: 180, | |
| height: 180, | |
| borderRadius: "50%", | |
| marginBottom: 50, | |
| border: "6px solid rgba(255, 255, 255, 0.8)", | |
| display: "flex", | |
| alignItems: "center", | |
| justifyContent: "center", | |
| background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)", | |
| fontSize: 72, | |
| fontWeight: 700, | |
| color: "#ffffff", | |
| boxShadow: "0 8px 32px rgba(0, 0, 0, 0.12)", | |
| }} | |
| > | |
| {agent.name.charAt(0).toUpperCase()} | |
| </div> | |
| )} | |
| <div | |
| style={{ | |
| fontSize: 80, | |
| fontWeight: 700, | |
| marginBottom: 30, | |
| color: "#1a1a1a", | |
| textAlign: "center", | |
| letterSpacing: "-0.02em", | |
| }} | |
| > | |
| {agent.name} | |
| </div> | |
| <div | |
| style={{ | |
| fontSize: 24, | |
| color: "#10b981", | |
| fontWeight: 600, | |
| marginBottom: 60, | |
| padding: "8px 16px", | |
| backgroundColor: "rgba(16, 185, 129, 0.1)", | |
| borderRadius: "12px", | |
| }} | |
| > | |
| Ready to chat | |
| </div> | |
| <div | |
| style={{ | |
| position: "absolute", | |
| bottom: 50, | |
| display: "flex", | |
| alignItems: "center", | |
| gap: 16, | |
| padding: "16px 24px", | |
| backgroundColor: "rgba(255, 255, 255, 0.95)", | |
| borderRadius: "20px", | |
| border: "1px solid rgba(0, 0, 0, 0.06)", | |
| boxShadow: "0 4px 20px rgba(0, 0, 0, 0.08)", | |
| }} | |
| > | |
| {logoSrc && ( | |
| <img src={logoSrc} alt="ConnectLabs Logo" width={32} height={32} /> | |
| )} | |
| <div | |
| style={{ | |
| fontSize: 18, | |
| color: "#666666", | |
| fontWeight: 600, | |
| }} | |
| > | |
| ConnectLabs.ai | |
| </div> | |
| </div> | |
| </div>, | |
| { | |
| width: 1200, | |
| height: 630, | |
| fonts: await getFonts(), | |
| }, | |
| ); | |
| const resvg = new Resvg(svg); | |
| const pngData = resvg.render(); | |
| return new Response(pngData.asPng(), { | |
| headers: { | |
| "Content-Type": "image/png", | |
| "Cache-Control": "public, max-age=3600", | |
| }, | |
| }); | |
| } catch (error) { | |
| console.error("Failed to generate OG image:", error); | |
| throw error; | |
| } | |
| } | |
| async function generateNotFoundImage() { | |
| try { | |
| const logoSrc = await convertLogoToPNG(); | |
| const svg = await satori( | |
| <div | |
| style={{ | |
| height: "100%", | |
| width: "100%", | |
| display: "flex", | |
| flexDirection: "column", | |
| alignItems: "center", | |
| justifyContent: "center", | |
| backgroundColor: "#ffffff", | |
| backgroundImage: "radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.08) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.08) 0%, transparent 50%)", | |
| color: "#000000", | |
| fontFamily: "Lexend", | |
| position: "relative", | |
| }} | |
| > | |
| <div style={{ fontSize: 60, fontWeight: 700 }}>Agent Not Found</div> | |
| <div style={{ fontSize: 24, color: "#666666", marginTop: 20 }}> | |
| This agent is not available | |
| </div> | |
| <div | |
| style={{ | |
| position: "absolute", | |
| bottom: 50, | |
| display: "flex", | |
| alignItems: "center", | |
| gap: 16, | |
| padding: "16px 24px", | |
| backgroundColor: "rgba(255, 255, 255, 0.95)", | |
| borderRadius: "20px", | |
| border: "1px solid rgba(0, 0, 0, 0.06)", | |
| boxShadow: "0 4px 20px rgba(0, 0, 0, 0.08)", | |
| }} | |
| > | |
| {logoSrc && ( | |
| <img src={logoSrc} alt="ConnectLabs Logo" width={32} height={32} /> | |
| )} | |
| <div | |
| style={{ | |
| fontSize: 18, | |
| color: "#666666", | |
| fontWeight: 600, | |
| }} | |
| > | |
| ConnectLabs.ai | |
| </div> | |
| </div> | |
| </div>, | |
| { | |
| width: 1200, | |
| height: 630, | |
| fonts: await getFonts(), | |
| }, | |
| ); | |
| const resvg = new Resvg(svg); | |
| const pngData = resvg.render(); | |
| return new Response(pngData.asPng(), { | |
| status: 404, | |
| headers: { | |
| "Content-Type": "image/png", | |
| "Cache-Control": "public, max-age=300", | |
| }, | |
| }); | |
| } catch (error) { | |
| console.error("Failed to generate not found image:", error); | |
| return new Response("Agent not found", { | |
| status: 404, | |
| headers: { | |
| "Content-Type": "text/plain", | |
| }, | |
| }); | |
| } | |
| } | |
| export const ServerRoute = createServerFileRoute( | |
| "/agent/$agentId/og-image", | |
| ).methods({ | |
| GET: async ({ params }) => { | |
| const { agentId } = params; | |
| const convex = new ConvexHttpClient(process.env.VITE_CONVEX_URL as string); | |
| try { | |
| const agent = await convex.query(api.agents.getPublicAgent, { | |
| id: agentId as Id<"agents">, | |
| }); | |
| if (!agent) { | |
| return await generateNotFoundImage(); | |
| } | |
| return await generateOGImage(agent); | |
| } catch (error) { | |
| console.error("Server route error:", error); | |
| return new Response("Error generating image", { | |
| status: 500, | |
| headers: { | |
| "Content-Type": "text/plain", | |
| }, | |
| }); | |
| } | |
| }, | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment