feat: add generateMetadata, enriched preset data, and JSON-LD to game page

This commit is contained in:
2026-04-27 15:40:27 +08:00
parent f228800593
commit e40417a02d
+90 -18
View File
@@ -1,3 +1,4 @@
import type { Metadata } from "next"
import { notFound } from "next/navigation"
import { after } from "next/server"
import { db } from "@/lib/db/index"
@@ -8,13 +9,64 @@ import {
gameComments,
gamePlatformSupport,
hardware,
user,
} from "@/lib/db/schema"
import { and, desc, eq, sql } from "drizzle-orm"
import { isSyncStale, syncSteamGame } from "@/lib/steam/sync"
import { GamePageClient } from "./game-page-client"
export const metadata = {
title: "Game",
async function resolveGame(id: string) {
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]
}
return game
}
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
const { id } = await params
const game = await resolveGame(id)
if (!game) {
return { title: "Game Not Found | DeckyVault" }
}
const description = game.description
? game.description.slice(0, 160)
: `Find benchmarks, community presets, and performance settings for ${game.title} on Steam Deck.`
return {
title: `${game.title} - Benchmarks & Settings`,
description,
alternates: { canonical: `https://deckyvault.xyz/game/${game.id}` },
openGraph: {
title: `${game.title} - Benchmarks & Settings | DeckyVault`,
description: game.description?.slice(0, 200) ?? `Benchmarks and settings for ${game.title}`,
url: `https://deckyvault.xyz/game/${game.id}`,
images: [{ url: `/game/${game.id}/opengraph-image`, width: 1200, height: 630 }],
type: "website",
siteName: "DeckyVault",
},
twitter: {
card: "summary_large_image",
title: `${game.title} - Benchmarks & Settings | DeckyVault`,
description: game.description?.slice(0, 200) ?? `Benchmarks and settings for ${game.title}`,
images: [`/game/${game.id}/opengraph-image`],
},
}
}
async function createGameStub(steamAppId: number) {
@@ -100,22 +152,7 @@ export default async function GamePage({
const forceSync = sync === "1"
// ── Resolve game ────────────────────────────────────────────────
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]
}
let game = await resolveGame(id)
if (!game && isNumeric) {
try {
@@ -184,10 +221,18 @@ export default async function GamePage({
protonVersion: performanceEntries.protonVersion,
osVersion: performanceEntries.osVersion,
createdAt: performanceEntries.createdAt,
userId: performanceEntries.userId,
userName: user.name,
userImage: user.image,
downvotes: performanceEntries.downvotes,
launchOptions: performanceEntries.launchOptions,
userNotes: performanceEntries.userNotes,
verifiedAt: performanceEntries.verifiedAt,
})
.from(performanceEntries)
.innerJoin(gameVersions, eq(performanceEntries.versionId, gameVersions.id))
.innerJoin(hardware, eq(performanceEntries.hardwareSlug, hardware.slug))
.innerJoin(user, eq(performanceEntries.userId, user.id))
.where(
and(
eq(gameVersions.gameId, game.id),
@@ -249,9 +294,35 @@ export default async function GamePage({
protonVersion: p.protonVersion,
osVersion: p.osVersion,
createdAt: p.createdAt.toISOString(),
settingsJson: p.settingsJson,
launchOptions: p.launchOptions,
userNotes: p.userNotes,
userId: p.userId,
userName: p.userName,
userImage: p.userImage,
downvotes: p.downvotes,
verifiedAt: p.verifiedAt ? p.verifiedAt.toISOString() : null,
}))
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "VideoGame",
name: game.title,
...(game.developer && { developer: { "@type": "Organization", name: game.developer } }),
...(game.genres && game.genres.length > 0 && { genre: game.genres }),
...(game.headerImage && { image: game.headerImage }),
url: `https://deckyvault.xyz/game/${game.id}`,
applicationCategory: "Game",
operatingSystem: "SteamOS",
...(game.storeUrl && { offers: { "@type": "Offer", url: game.storeUrl } }),
}),
}}
/>
<GamePageClient
game={serializedGame}
counts={{
@@ -263,5 +334,6 @@ export default async function GamePage({
presets={serializedPresets}
gameId={game.id}
/>
</>
)
}