feat: redesign device detail page with fixes and improvements

- Fix upscalerBreakdown/fsrBreakdown property name mismatch
- Fix deviceTypeLabel map (handled → handheld)
- Fix device color to use per-device colorIndex instead of hardcoded 0
- Add image/icon hero section with device color accent
- Add error state with retry button
- Add generateStaticParams with ISR (1-hour revalidation)
- Enhanced SEO metadata with keywords, OG, Twitter cards
- Fix JSON-LD to use 'Handheld' label instead of raw enum value
This commit is contained in:
2026-04-27 20:55:15 +08:00
parent eb944c2b3c
commit 060911e29d
2 changed files with 209 additions and 65 deletions
+77 -7
View File
@@ -3,25 +3,76 @@ import { hardware } from "@/lib/db/schema"
import { eq } from "drizzle-orm"
import { notFound } from "next/navigation"
import { DeviceDetailClient } from "./device-detail-client"
import type { Metadata } from "next"
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
export const revalidate = 3600
export async function generateStaticParams() {
const devices = await db
.select({ slug: hardware.slug })
.from(hardware)
return devices.map((d) => ({ slug: d.slug }))
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>
}): Promise<Metadata> {
const { slug } = await params
const [device] = await db
.select({ name: hardware.name })
.select({
name: hardware.name,
deviceType: hardware.deviceType,
})
.from(hardware)
.where(eq(hardware.slug, slug))
.limit(1)
if (!device) return { title: "Device Not Found — DeckyVault" }
const typeLabel = device.deviceType === "handheld" ? "Handheld" : "Console"
return {
title: `${device.name} — DeckyVault`,
description: `Benchmark data and performance stats for ${device.name} on DeckyVault`,
description: `Benchmark data, FPS stats, and performance analysis for the ${device.name} (${typeLabel}) on DeckyVault.`,
keywords: [
device.name.toLowerCase(),
device.deviceType,
"benchmarks",
"FPS",
"performance",
"steam deck",
],
alternates: { canonical: `https://deckyvault.xyz/devices/${slug}` },
openGraph: {
title: `${device.name} — DeckyVault`,
description: `Benchmark data and performance stats for ${device.name} on DeckyVault.`,
url: `https://deckyvault.xyz/devices/${slug}`,
siteName: "DeckyVault",
type: "website",
images: [
{
url: `/devices/${slug}/opengraph-image`,
width: 1200,
height: 630,
},
],
},
twitter: {
card: "summary_large_image",
title: `${device.name} — DeckyVault`,
description: `Benchmark data and performance stats for ${device.name} on DeckyVault.`,
images: [`/devices/${slug}/opengraph-image`],
},
}
}
export default async function DevicePage({ params }: { params: Promise<{ slug: string }> }) {
export default async function DevicePage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const [device] = await db
@@ -29,6 +80,7 @@ export default async function DevicePage({ params }: { params: Promise<{ slug: s
slug: hardware.slug,
name: hardware.name,
deviceType: hardware.deviceType,
image: hardware.image,
})
.from(hardware)
.where(eq(hardware.slug, slug))
@@ -38,18 +90,36 @@ export default async function DevicePage({ params }: { params: Promise<{ slug: s
notFound()
}
const allDevices = await db
.select({ slug: hardware.slug })
.from(hardware)
.orderBy(hardware.sortOrder)
const deviceColorIndex = allDevices.findIndex((d) => d.slug === slug)
const typeLabel = device.deviceType === "handheld" ? "Handheld" : "Console"
const jsonLd = {
"@context": "https://schema.org",
"@type": "Product",
name: device.name,
category: device.deviceType,
category: typeLabel,
url: `https://deckyvault.xyz/devices/${device.slug}`,
}
return (
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<DeviceDetailClient device={device} />
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<DeviceDetailClient
device={{
slug: device.slug,
name: device.name,
deviceType: device.deviceType,
image: device.image,
colorIndex: deviceColorIndex,
}}
/>
</>
)
}