From 9a6db98d42093af657b0cc3902d8ab061abb98e9 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Mon, 27 Apr 2026 16:02:20 +0800 Subject: [PATCH] feat: add dynamic OG image generation for game pages --- app/game/[id]/opengraph-image.tsx | 81 +++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 app/game/[id]/opengraph-image.tsx diff --git a/app/game/[id]/opengraph-image.tsx b/app/game/[id]/opengraph-image.tsx new file mode 100644 index 0000000..457e444 --- /dev/null +++ b/app/game/[id]/opengraph-image.tsx @@ -0,0 +1,81 @@ +import { ImageResponse } from "next/og" +import { db } from "@/lib/db/index" +import { games, gameVersions, performanceEntries } from "@/lib/db/schema" +import { eq, and, sql } from "drizzle-orm" +import { readFile } from "node:fs/promises" +import { join } from "node:path" + +export const alt = "DeckyVault - Game Benchmarks" +export const size = { width: 1200, height: 630 } +export const contentType = "image/png" + +export default async function Image({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params + const isNumeric = /^\d+$/.test(id) + + let game + if (isNumeric) { + const rows = await db.select().from(games).where(eq(games.steamAppId, Number(id))).limit(1) + game = rows[0] + } else { + const rows = await db.select().from(games).where(eq(games.id, id)).limit(1) + game = rows[0] + } + + if (!game) { + return new ImageResponse( + ( +
+
Game Not Found
+
DeckyVault
+
+ ), + { ...size } + ) + } + + // Get best FPS stat + let avgFps: number | null = null + try { + const [bestStat] = await db + .select({ avgFps: sql`round(avg(${performanceEntries.fpsAvg})::numeric, 1)` }) + .from(performanceEntries) + .innerJoin(gameVersions, eq(performanceEntries.versionId, gameVersions.id)) + .where(and(eq(gameVersions.gameId, game.id), eq(performanceEntries.isRemoved, false))) + .limit(1) + avgFps = bestStat?.avgFps ?? null + } catch { + // No FPS data available — that's fine + } + + const logoData = await readFile(join(process.cwd(), "app/icon.png"), "base64") + const logoSrc = `data:image/png;base64,${logoData}` + + return new ImageResponse( + ( +
+
+ + DeckyVault +
+
+ {game.title} +
+
+ {game.developer && by {game.developer}} + {avgFps !== null && ~{avgFps} avg FPS} +
+ {game.genres && game.genres.length > 0 && ( +
+ {game.genres.slice(0, 4).map((genre: string) => ( + + {genre} + + ))} +
+ )} +
+ ), + { ...size } + ) +}