feat: add generateMetadata, enriched preset data, and JSON-LD to game page
This commit is contained in:
+101
-29
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Metadata } from "next"
|
||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
import { after } from "next/server"
|
import { after } from "next/server"
|
||||||
import { db } from "@/lib/db/index"
|
import { db } from "@/lib/db/index"
|
||||||
@@ -8,13 +9,64 @@ import {
|
|||||||
gameComments,
|
gameComments,
|
||||||
gamePlatformSupport,
|
gamePlatformSupport,
|
||||||
hardware,
|
hardware,
|
||||||
|
user,
|
||||||
} from "@/lib/db/schema"
|
} from "@/lib/db/schema"
|
||||||
import { and, desc, eq, sql } from "drizzle-orm"
|
import { and, desc, eq, sql } from "drizzle-orm"
|
||||||
import { isSyncStale, syncSteamGame } from "@/lib/steam/sync"
|
import { isSyncStale, syncSteamGame } from "@/lib/steam/sync"
|
||||||
import { GamePageClient } from "./game-page-client"
|
import { GamePageClient } from "./game-page-client"
|
||||||
|
|
||||||
export const metadata = {
|
async function resolveGame(id: string) {
|
||||||
title: "Game",
|
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) {
|
async function createGameStub(steamAppId: number) {
|
||||||
@@ -100,22 +152,7 @@ export default async function GamePage({
|
|||||||
const forceSync = sync === "1"
|
const forceSync = sync === "1"
|
||||||
|
|
||||||
// ── Resolve game ────────────────────────────────────────────────
|
// ── Resolve game ────────────────────────────────────────────────
|
||||||
let game
|
let game = await resolveGame(id)
|
||||||
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 && isNumeric) {
|
if (!game && isNumeric) {
|
||||||
try {
|
try {
|
||||||
@@ -184,10 +221,18 @@ export default async function GamePage({
|
|||||||
protonVersion: performanceEntries.protonVersion,
|
protonVersion: performanceEntries.protonVersion,
|
||||||
osVersion: performanceEntries.osVersion,
|
osVersion: performanceEntries.osVersion,
|
||||||
createdAt: performanceEntries.createdAt,
|
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)
|
.from(performanceEntries)
|
||||||
.innerJoin(gameVersions, eq(performanceEntries.versionId, gameVersions.id))
|
.innerJoin(gameVersions, eq(performanceEntries.versionId, gameVersions.id))
|
||||||
.innerJoin(hardware, eq(performanceEntries.hardwareSlug, hardware.slug))
|
.innerJoin(hardware, eq(performanceEntries.hardwareSlug, hardware.slug))
|
||||||
|
.innerJoin(user, eq(performanceEntries.userId, user.id))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(gameVersions.gameId, game.id),
|
eq(gameVersions.gameId, game.id),
|
||||||
@@ -249,19 +294,46 @@ export default async function GamePage({
|
|||||||
protonVersion: p.protonVersion,
|
protonVersion: p.protonVersion,
|
||||||
osVersion: p.osVersion,
|
osVersion: p.osVersion,
|
||||||
createdAt: p.createdAt.toISOString(),
|
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 (
|
return (
|
||||||
<GamePageClient
|
<>
|
||||||
game={serializedGame}
|
<script
|
||||||
counts={{
|
type="application/ld+json"
|
||||||
benchmarks: benchmarkCount,
|
dangerouslySetInnerHTML={{
|
||||||
presets: presetCount,
|
__html: JSON.stringify({
|
||||||
comments: commentCount,
|
"@context": "https://schema.org",
|
||||||
}}
|
"@type": "VideoGame",
|
||||||
platformSupport={platformSupport}
|
name: game.title,
|
||||||
presets={serializedPresets}
|
...(game.developer && { developer: { "@type": "Organization", name: game.developer } }),
|
||||||
gameId={game.id}
|
...(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={{
|
||||||
|
benchmarks: benchmarkCount,
|
||||||
|
presets: presetCount,
|
||||||
|
comments: commentCount,
|
||||||
|
}}
|
||||||
|
platformSupport={platformSupport}
|
||||||
|
presets={serializedPresets}
|
||||||
|
gameId={game.id}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user