Skip to content

Instantly share code, notes, and snippets.

@vanxh
Created July 29, 2025 22:35
Show Gist options
  • Select an option

  • Save vanxh/c1b450a80e24bfb2a3e7500918f92df0 to your computer and use it in GitHub Desktop.

Select an option

Save vanxh/c1b450a80e24bfb2a3e7500918f92df0 to your computer and use it in GitHub Desktop.
Tanstack start dynamic OG image creation
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