feat: add comprehensive SEO (metadata, OG images, sitemap, robots, manifest) and update README

This commit is contained in:
2026-04-25 01:52:28 +08:00
parent 4688f3f594
commit 0dd1e86487
21 changed files with 1075 additions and 132 deletions
+56
View File
@@ -0,0 +1,56 @@
# =============================================================================
# Environment Configuration
# =============================================================================
# Copy this file to .env.local and fill in your actual values
# Never commit .env.local to version control
# =============================================================================
# -----------------------------------------------------------------------------
# SYSTEM & DEPLOYMENT
# -----------------------------------------------------------------------------
NIXPACKS_NODE_VERSION=22
# -----------------------------------------------------------------------------
# DATABASE
# -----------------------------------------------------------------------------
# PostgreSQL connection string
DATABASE_URL="postgresql://user:password@localhost:5432/grounds"
# -----------------------------------------------------------------------------
# APPLICATION URLS
# -----------------------------------------------------------------------------
# Base URL of the application (client-accessible)
NEXT_PUBLIC_SITE_URL="https://localhost:3000"
# Auth callback URL
NEXT_PUBLIC_APP_URL="https://localhost:3000"
# -----------------------------------------------------------------------------
# BETTER AUTH
# -----------------------------------------------------------------------------
# Better Auth configuration
# URL must match your app URL (with protocol)
BETTER_AUTH_URL="https://localhost:3000"
# Secret key for signing tokens (generate with: openssl rand -base64 32)
BETTER_AUTH_SECRET="your-secret-key-here"
# -----------------------------------------------------------------------------
# AUTHENTICATION
# -----------------------------------------------------------------------------
# Google OAuth credentials (required)
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
# Discord OAuth credentials (required)
DISCORD_CLIENT_ID=""
DISCORD_CLIENT_SECRET=""
# -----------------------------------------------------------------------------
# STORAGE (S3 COMPATIBLE)
# -----------------------------------------------------------------------------
# S3-compatible credentials for file storage
S3_ACCESS_KEY_ID=""
S3_SECRET_ACCESS_KEY=""
S3_BUCKET_NAME=""
S3_PUBLIC_URL=""
+1
View File
@@ -32,6 +32,7 @@ yarn-error.log*
# env files (can opt-in for committing if needed) # env files (can opt-in for committing if needed)
.env* .env*
!.env.example
# vercel # vercel
.vercel .vercel
+33 -20
View File
@@ -2,11 +2,11 @@
An open-source, community-driven database for Steam Deck (OLED & LCD) and Steam Machine compatibility, performance metrics, and settings. An open-source, community-driven database for Steam Deck (OLED & LCD) and Steam Machine compatibility, performance metrics, and settings.
[//]: # "Add shields here if available: e.g., license, contributors, last commit" > **Note:** DeckyVault is currently under active development. The site is live at [deckyvault.xyz](https://deckyvault.xyz) but features are being built incrementally. Stay tuned!
## Why DeckyVault? ## Why DeckyVault?
Gaming on the Steam Deck is incredible, but finding reliable, detailed answers about whether a game will run wellor how to optimize itcan be frustrating. Existing resources are often fragmented, outdated, or lack the depth the community needs. Gaming on the Steam Deck is incredible, but finding reliable, detailed answers about whether a game will run wellor how to optimize itcan be frustrating. Existing resources are often fragmented, outdated, or lack the depth the community needs.
**DeckyVault** aims to solve this by being the definitive, community-first hub for Steam Deck performance data. **DeckyVault** aims to solve this by being the definitive, community-first hub for Steam Deck performance data.
@@ -33,36 +33,49 @@ Gaming on the Steam Deck is incredible, but finding reliable, detailed answers a
## Tech Stack ## Tech Stack
- **[Next.js](https://nextjs.org)** — React framework for production - **[Next.js 16](https://nextjs.org)** — React framework (App Router)
- **[React](https://react.dev)** — UI library - **[React 19](https://react.dev)** — UI library
- **[TypeScript](https://www.typescriptlang.org)** — Type safety - **[TypeScript](https://www.typescriptlang.org)** — Type safety
- **[Tailwind CSS](https://tailwindcss.com)** — Utility-first styling *(verify in your local setup)* - **[Tailwind CSS v4](https://tailwindcss.com)** — Utility-first styling
- **[Elysia](https://elysiajs.com)** — Backend API framework
- **[Drizzle ORM](https://orm.drizzle.team)** — Type-safe database queries
- **[PostgreSQL](https://www.postgresql.org)** — Primary database
- **[better-auth](https://better-auth.com)** — Authentication (Google & Discord OAuth)
- **[Tiptap](https://tiptap.dev)** — Rich text editor
- **[Motion](https://motion.dev)** — Animations
- **[AWS S3](https://aws.amazon.com/s3/)** — File storage
- **[Bun](https://bun.sh)** — Package manager & runtime
## Getting Started ## Getting Started
First, clone the repository and install dependencies: Clone the repository and install dependencies:
```bash ```bash
npm install bun install
# or
yarn install
# or
pnpm install
``` ```
Then, run the development server: Copy the environment file and configure your values:
```bash ```bash
npm run dev cp .env.example .env.local
# or
yarn dev
# or
pnpm dev
``` ```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. Run the development server:
You can start editing the pages by modifying files in the `app/` directory. The page auto-updates as you edit the file. ```bash
bun run dev
```
Open [http://localhost:3000](http://localhost:3000) to see the result.
## Scripts
| Command | Description |
|---------|-------------|
| `bun run dev` | Start development server (HTTPS) |
| `bun run build` | Create production build |
| `bun run start` | Start production server |
| `bun run lint` | Run ESLint |
## Contributing ## Contributing
@@ -73,7 +86,7 @@ We welcome contributions of all kinds! Whether you want to:
- Improve the design - Improve the design
- Contribute code - Contribute code
Feel free to open an [issue](../../issues) or submit a [pull request](../../pulls). Feel free to open an [issue](https://github.com/AdrianBonpin/deckyvault/issues) or submit a [pull request](https://github.com/AdrianBonpin/deckyvault/pulls).
If you're interested in helping develop or design the project, don't hesitate to reach out! If you're interested in helping develop or design the project, don't hesitate to reach out!
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

+11 -20
View File
@@ -1,26 +1,17 @@
@import "tailwindcss"; @import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline { @theme inline {
--color-background: var(--background); /* Colors */
--color-foreground: var(--foreground); --color-background: #100b14;
--font-sans: var(--font-geist-sans); --color-foreground: #180161;
--font-mono: var(--font-geist-mono); --color-primary: #eb3779;
} --color-secondary: #571b8b;
--color-accent: #fb793c;
--color-text: #ebe4f1;
@media (prefers-color-scheme: dark) { /* Defaults */
:root { --default-transition-duration: 0.3s;
--background: #0a0a0a;
--foreground: #ededed;
}
}
body { /* Fonts */
background: var(--background); --font-sans: var(--font-lexend);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
} }
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

+79 -19
View File
@@ -1,33 +1,93 @@
import type { Metadata } from "next"; import type { Metadata } from "next"
import { Geist, Geist_Mono } from "next/font/google"; import { Lexend } from "next/font/google"
import "./globals.css"; import "./globals.css"
import Script from "next/script"
const geistSans = Geist({ const font = Lexend({
variable: "--font-geist-sans", variable: "--font-lexend",
subsets: ["latin"], subsets: ["latin"],
}); })
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", metadataBase: new URL("https://deckyvault.xyz"),
description: "Generated by create next app", title: {
}; default: "DeckyVault - Steam Deck Benchmarks & Settings",
template: "%s | DeckyVault",
},
description:
"A fast, modern browser for finding game benchmarks, settings, and guides for Steam Deck OLED, Steam Deck LCD, and Steam Machine.",
keywords: [
"Steam Deck",
"benchmarks",
"settings",
"performance",
"FPS",
"gaming",
"Steam Machine",
"Proton",
"FSR",
"compatibility",
],
authors: [
{ name: "Adrian Bonpin", url: "https://github.com/AdrianBonpin" },
],
creator: "@adrianbonpin",
openGraph: {
type: "website",
locale: "en_US",
url: "https://deckyvault.xyz",
siteName: "DeckyVault",
title: "DeckyVault - Steam Deck Benchmarks & Settings",
description:
"A fast, modern browser for finding game benchmarks, settings, and guides for Steam Deck OLED, Steam Deck LCD, and Steam Machine.",
images: [
{
url: "/opengraph-image",
width: 1200,
height: 630,
alt: "DeckyVault - Steam Deck Benchmarks & Settings",
},
],
},
twitter: {
card: "summary_large_image",
title: "DeckyVault - Steam Deck Benchmarks & Settings",
description:
"A fast, modern browser for finding game benchmarks, settings, and guides for Steam Deck OLED, Steam Deck LCD, and Steam Machine.",
creator: "@adrianbonpin",
images: ["/twitter-image"],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
}
export default function RootLayout({ export default function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode
}>) { }>) {
return ( return (
<html <html
lang="en" lang='en'
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} className={`${font.variable} bg-background text-text antialiased overscroll-none`}
> >
<body className="min-h-full flex flex-col">{children}</body> <body className='min-h-full flex flex-col relative'>
{children}
<Script
src='https://analytics.ranlabs.space/api/script.js'
data-site-id='b9817e8df599'
strategy='afterInteractive'
/>
</body>
</html> </html>
); )
} }
+21
View File
@@ -0,0 +1,21 @@
import type { MetadataRoute } from "next"
export default function manifest(): MetadataRoute.Manifest {
return {
name: "DeckyVault - Steam Deck Benchmarks & Settings",
short_name: "DeckyVault",
description:
"A fast, modern browser for finding game benchmarks, settings, and guides for Steam Deck OLED, Steam Deck LCD, and Steam Machine.",
start_url: "/",
display: "standalone",
background_color: "#100b14",
theme_color: "#eb3779",
icons: [
{
src: "/icon.png",
sizes: "any",
type: "image/png",
},
],
}
}
+65
View File
@@ -0,0 +1,65 @@
import { ImageResponse } from "next/og"
import { readFile } from "node:fs/promises"
import { join } from "node:path"
export const alt = "DeckyVault - Steam Deck Benchmarks & Settings"
export const size = {
width: 1200,
height: 630,
}
export const contentType = "image/png"
export default async function Image() {
const logoData = await readFile(
join(process.cwd(), "app/icon.png"),
"base64"
)
const logoSrc = `data:image/png;base64,${logoData}`
return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
background: "#100b14",
color: "#ebe4f1",
fontFamily: "sans-serif",
gap: "16px",
}}
>
<img
src={logoSrc}
alt="DeckyVault"
height={120}
style={{ borderRadius: "16px" }}
/>
<div
style={{
fontSize: 64,
fontWeight: 700,
letterSpacing: "-0.02em",
}}
>
DeckyVault
</div>
<div
style={{
fontSize: 28,
fontWeight: 400,
opacity: 0.8,
}}
>
Steam Deck Benchmarks & Settings
</div>
</div>
),
{
...size,
}
)
}
+80 -58
View File
@@ -1,65 +1,87 @@
import Image from "next/image"; "use client"
import { AnimatePresence, motion } from "motion/react"
import { useState } from "react"
import logo from "@/app/icon.png"
import Image from "next/image"
import Link from "next/link"
export default function Landing() {
const words = ["benchmarks", "settings", "guides", "reviews"]
const [currentWord, setCurrentWord] = useState(0)
setTimeout(() => {
setCurrentWord((currentWord + 1) % words.length)
}, 2000)
export default function Home() {
return ( return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black"> <section
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start"> id='hero'
<Image className='w-dvw h-dvh flex flex-col items-center justify-center relative'
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
> >
<Image <Image
className="dark:invert" src={logo}
src="/vercel.svg" alt=''
alt="Vercel logomark" className='h-30 w-auto'
width={16} loading='eager'
height={16}
/> />
Deploy Now <motion.h1
</a> initial={{ opacity: 0 }}
<a animate={{ opacity: 1 }}
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]" className='font-bold text-5xl text-center'
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
> >
Documentation DeckyVault
</a> </motion.h1>
</div> <motion.h2
</main> initial={{
</div> opacity: 0,
); }}
animate={{ opacity: 1, transition: { delay: 0.5 } }}
className='mt-4 flex flex-col items-center font-semibold text-2xl'
>
<motion.span
key='intro'
className='text-center'
>
A fast, modern browser for finding game
</motion.span>
<AnimatePresence
mode='wait'
initial={false}
>
<motion.span
key={currentWord}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className='text-primary font-bold'
>
{words[currentWord]}
</motion.span>
</AnimatePresence>
</motion.h2>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 0.6, transition: { delay: 1 } }}
className='mt-4 text-center text-lg max-w-xl'
>
Stay tuned for the launch of DeckyVault
</motion.p>
<motion.small
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 1.5 } }}
className='absolute bottom-4 text-center text-xs flex flex-row gap-1'
>
<span className='opacity-60'>2026 DeckyVault.</span>
<Link
title='Visit our Github Repository'
href='https://github.com/AdrianBonpin/deckyvault'
className='text-accent opacity-60 hover:opacity-100 transition-opacity'
>
Github.
</Link>
</motion.small>
</section>
)
} }
+13
View File
@@ -0,0 +1,13 @@
import type { MetadataRoute } from "next"
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: "https://deckyvault.xyz/sitemap.xml",
host: "https://deckyvault.xyz",
}
}
+21
View File
@@ -0,0 +1,21 @@
import type { MetadataRoute } from "next"
const BASE_URL = "https://deckyvault.xyz"
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: BASE_URL,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 1,
},
// Future dynamic routes can be added here:
// {
// url: `${BASE_URL}/games/${game.slug}`,
// lastModified: game.updatedAt,
// changeFrequency: "weekly",
// priority: 0.8,
// },
]
}
+65
View File
@@ -0,0 +1,65 @@
import { ImageResponse } from "next/og"
import { readFile } from "node:fs/promises"
import { join } from "node:path"
export const alt = "DeckyVault - Steam Deck Benchmarks & Settings"
export const size = {
width: 1200,
height: 630,
}
export const contentType = "image/png"
export default async function Image() {
const logoData = await readFile(
join(process.cwd(), "app/icon.png"),
"base64"
)
const logoSrc = `data:image/png;base64,${logoData}`
return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
background: "#100b14",
color: "#ebe4f1",
fontFamily: "sans-serif",
gap: "16px",
}}
>
<img
src={logoSrc}
alt="DeckyVault"
height={120}
style={{ borderRadius: "16px" }}
/>
<div
style={{
fontSize: 64,
fontWeight: 700,
letterSpacing: "-0.02em",
}}
>
DeckyVault
</div>
<div
style={{
fontSize: 28,
fontWeight: 400,
opacity: 0.8,
}}
>
Steam Deck Benchmarks & Settings
</div>
</div>
),
{
...size,
}
)
}
+3
View File
@@ -15,6 +15,7 @@
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"drizzle-orm": "^0.45.2", "drizzle-orm": "^0.45.2",
"elysia": "^1.4.28", "elysia": "^1.4.28",
"lucide-react": "^1.11.0",
"motion": "^12.38.0", "motion": "^12.38.0",
"next": "16.2.4", "next": "16.2.4",
"pg": "^8.20.0", "pg": "^8.20.0",
@@ -1042,6 +1043,8 @@
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lucide-react": ["lucide-react@1.11.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UOhjdztXCgdBReRcIhsvz2siIBogfv/lhJEIViCpLt924dO+GDms9T7DNoucI23s6kEPpe988m5N0D2ajnzb2g=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
@@ -0,0 +1,616 @@
# SEO & README Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add comprehensive SEO infrastructure (metadata, OG images, sitemap, robots.txt, manifest) and update the README to reflect the current project state.
**Architecture:** All SEO files live in `app/` as Next.js file conventions. The OG image is dynamically generated using `ImageResponse` from `next/og`, leveraging the existing `app/icon.png` logo. Sitemap and manifest use TypeScript file conventions for future dynamic data readiness. The site URL is `https://deckyvault.xyz`.
**Tech Stack:** Next.js 16.2.4 App Router, `next/og` ImageResponse, TypeScript, Tailwind CSS v4
---
## File Structure
### Files to Create
| File | Purpose |
|------|---------|
| `app/layout.tsx` | Modify: add full metadata (title template, description, OpenGraph, Twitter, authors, keywords, metadataBase) |
| `app/opengraph-image.tsx` | Create: dynamic OG banner image using ImageResponse with logo + brand colors |
| `app/twitter-image.tsx` | Create: Twitter card image (can share OG image logic or be identical) |
| `app/sitemap.ts` | Create: programmatic sitemap with static routes, ready for dynamic data |
| `app/robots.ts` | Create: robots.txt allowing all crawlers, pointing to sitemap |
| `app/manifest.ts` | Create: PWA web manifest with app name, theme colors, icons |
| `app/icon.png` | Already exists — used as favicon and in OG image generation |
| `README.md` | Modify: update tech stack, install commands, development status note |
### Files Unchanged
| File | Reason |
|------|--------|
| `app/page.tsx` | Landing page — no SEO changes needed (metadata is in layout) |
| `app/globals.css` | Styles — no changes needed |
| `next.config.ts` | No config changes needed for this scope |
| `public/` | Stays empty — all assets handled via app directory conventions |
---
## Chunk 1: Root Layout Metadata & OG Image Generation
### Task 1: Enhance Root Layout Metadata
**Files:**
- Modify: `app/layout.tsx`
The current layout only sets `title`. We need to add full metadata for SEO and social sharing.
- [ ] **Step 1: Update `app/layout.tsx` with comprehensive metadata**
Replace the existing `metadata` export with a full configuration:
```tsx
import type { Metadata } from "next"
import { Lexend } from "next/font/google"
import "./globals.css"
const font = Lexend({
variable: "--font-lexend",
subsets: ["latin"],
})
export const metadata: Metadata = {
metadataBase: new URL("https://deckyvault.xyz"),
title: {
default: "DeckyVault - Steam Deck Benchmarks & Settings",
template: "%s | DeckyVault",
},
description:
"A fast, modern browser for finding game benchmarks, settings, and guides for Steam Deck OLED, Steam Deck LCD, and Steam Machine.",
keywords: [
"Steam Deck",
"benchmarks",
"settings",
"performance",
"FPS",
"gaming",
"Steam Machine",
"Proton",
"FSR",
"compatibility",
],
authors: [{ name: "Adrian Bonpin", url: "https://github.com/AdrianBonpin" }],
creator: "@adrianbonpin",
openGraph: {
type: "website",
locale: "en_US",
url: "https://deckyvault.xyz",
siteName: "DeckyVault",
title: "DeckyVault - Steam Deck Benchmarks & Settings",
description:
"A fast, modern browser for finding game benchmarks, settings, and guides for Steam Deck OLED, Steam Deck LCD, and Steam Machine.",
images: [
{
url: "/opengraph-image",
width: 1200,
height: 630,
alt: "DeckyVault - Steam Deck Benchmarks & Settings",
},
],
},
twitter: {
card: "summary_large_image",
title: "DeckyVault - Steam Deck Benchmarks & Settings",
description:
"A fast, modern browser for finding game benchmarks, settings, and guides for Steam Deck OLED, Steam Deck LCD, and Steam Machine.",
creator: "@adrianbonpin",
images: ["/twitter-image"],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html
lang='en'
className={`${font.variable} bg-background text-text antialiased overscroll-none`}
>
<body className='min-h-full flex flex-col relative'>
{children}
</body>
</html>
)
}
```
- [ ] **Step 2: Verify layout compiles**
Run: `bun run build`
Expected: Build succeeds with no TypeScript errors.
---
### Task 2: Create Dynamic OpenGraph Image
**Files:**
- Create: `app/opengraph-image.tsx`
Uses `ImageResponse` from `next/og` to generate a branded OG banner at build time. Reads the existing `app/icon.png` as a base64 data URI and renders it with the DeckyVault name and tagline on the brand background color.
- [ ] **Step 1: Create `app/opengraph-image.tsx`**
```tsx
import { ImageResponse } from "next/og"
import { readFile } from "node:fs/promises"
import { join } from "node:path"
export const alt = "DeckyVault - Steam Deck Benchmarks & Settings"
export const size = {
width: 1200,
height: 630,
}
export const contentType = "image/png"
export default async function Image() {
const logoData = await readFile(
join(process.cwd(), "app/icon.png"),
"base64"
)
const logoSrc = `data:image/png;base64,${logoData}`
return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
background: "#100b14",
color: "#ebe4f1",
fontFamily: "sans-serif",
gap: "16px",
}}
>
<img
src={logoSrc}
alt="DeckyVault"
height="120"
style={{ borderRadius: "16px" }}
/>
<div
style={{
fontSize: 64,
fontWeight: 700,
letterSpacing: "-0.02em",
}}
>
DeckyVault
</div>
<div
style={{
fontSize: 28,
fontWeight: 400,
opacity: 0.8,
}}
>
Steam Deck Benchmarks &amp; Settings
</div>
</div>
),
{
...size,
}
)
}
```
- [ ] **Step 2: Verify OG image builds**
Run: `bun run build`
Expected: Build succeeds. `opengraph-image` route appears in build output.
---
### Task 3: Create Twitter Card Image
**Files:**
- Create: `app/twitter-image.tsx`
Shares the same design as the OG image. Next.js supports separate `twitter-image` files. For now, we duplicate the OG image logic to allow independent customization later.
- [ ] **Step 1: Create `app/twitter-image.tsx`**
```tsx
import { ImageResponse } from "next/og"
import { readFile } from "node:fs/promises"
import { join } from "node:path"
export const alt = "DeckyVault - Steam Deck Benchmarks & Settings"
export const size = {
width: 1200,
height: 630,
}
export const contentType = "image/png"
export default async function Image() {
const logoData = await readFile(
join(process.cwd(), "app/icon.png"),
"base64"
)
const logoSrc = `data:image/png;base64,${logoData}`
return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
background: "#100b14",
color: "#ebe4f1",
fontFamily: "sans-serif",
gap: "16px",
}}
>
<img
src={logoSrc}
alt="DeckyVault"
height="120"
style={{ borderRadius: "16px" }}
/>
<div
style={{
fontSize: 64,
fontWeight: 700,
letterSpacing: "-0.02em",
}}
>
DeckyVault
</div>
<div
style={{
fontSize: 28,
fontWeight: 400,
opacity: 0.8,
}}
>
Steam Deck Benchmarks &amp; Settings
</div>
</div>
),
{
...size,
}
)
}
```
- [ ] **Step 2: Verify Twitter image builds**
Run: `bun run build`
Expected: Build succeeds. Both `opengraph-image` and `twitter-image` routes appear.
---
## Chunk 2: Sitemap, Robots.txt, and Manifest
### Task 4: Create Dynamic Sitemap
**Files:**
- Create: `app/sitemap.ts`
Programmatically generates `sitemap.xml`. Currently static (just the homepage), but structured so that dynamic game pages can be added later by querying the database.
- [ ] **Step 1: Create `app/sitemap.ts`**
```ts
import type { MetadataRoute } from "next"
const BASE_URL = "https://deckyvault.xyz"
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: BASE_URL,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 1,
},
// Future dynamic routes can be added here:
// {
// url: `${BASE_URL}/games/${game.slug}`,
// lastModified: game.updatedAt,
// changeFrequency: "weekly",
// priority: 0.8,
// },
]
}
```
- [ ] **Step 2: Verify sitemap builds**
Run: `bun run build`
Expected: Build succeeds. `/sitemap.xml` route available.
---
### Task 5: Create Robots.txt
**Files:**
- Create: `app/robots.ts`
Allows all crawlers, points to the sitemap, and sets the host.
- [ ] **Step 1: Create `app/robots.ts`**
```ts
import type { MetadataRoute } from "next"
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: "https://deckyvault.xyz/sitemap.xml",
host: "https://deckyvault.xyz",
}
}
```
- [ ] **Step 2: Verify robots.txt builds**
Run: `bun run build`
Expected: Build succeeds. `/robots.txt` route available.
---
### Task 6: Create Web App Manifest
**Files:**
- Create: `app/manifest.ts`
Generates `manifest.webmanifest` for PWA support and better mobile experience. Uses the existing `app/icon.png` and the brand colors from `globals.css`.
- [ ] **Step 1: Create `app/manifest.ts`**
```ts
import type { MetadataRoute } from "next"
export default function manifest(): MetadataRoute.Manifest {
return {
name: "DeckyVault - Steam Deck Benchmarks & Settings",
short_name: "DeckyVault",
description:
"A fast, modern browser for finding game benchmarks, settings, and guides for Steam Deck OLED, Steam Deck LCD, and Steam Machine.",
start_url: "/",
display: "standalone",
background_color: "#100b14",
theme_color: "#eb3779",
icons: [
{
src: "/icon.png",
sizes: "any",
type: "image/png",
},
],
}
}
```
Note: `app/icon.png` is served at `/icon.png` by Next.js (the existing build output confirms this route exists).
- [ ] **Step 2: Verify manifest builds**
Run: `bun run build`
Expected: Build succeeds. `/manifest.webmanifest` route available.
---
## Chunk 3: README Update
### Task 7: Update README.md
**Files:**
- Modify: `README.md`
The current README is outdated — references npm/yarn/pnpm instead of bun, doesn't mention Elysia, Drizzle, Tiptap, better-auth, S3 storage, or the actual full tech stack. Needs to reflect the current state and note that the site is under development.
- [ ] **Step 1: Replace `README.md` content**
```markdown
# DeckyVault
An open-source, community-driven database for Steam Deck (OLED & LCD) and Steam Machine compatibility, performance metrics, and settings.
> **Note:** DeckyVault is currently under active development. The site is live at [deckyvault.xyz](https://deckyvault.xyz) but features are being built incrementally. Stay tuned!
## Why DeckyVault?
Gaming on the Steam Deck is incredible, but finding reliable, detailed answers about whether a game will run well — or how to optimize it — can be frustrating. Existing resources are often fragmented, outdated, or lack the depth the community needs.
**DeckyVault** aims to solve this by being the definitive, community-first hub for Steam Deck performance data.
## Features
- **Community-First Contributions** — Built by the community, for the community. Anyone can contribute data and improvements.
- **Streamlined Filters & Searching** — Quickly find the games you're looking for with powerful search and filtering.
- **Rich Performance Metrics**
- Community average FPS (**low**, **avg**, **high**).
- Extended FPS metrics for technologies like **FSR** and **Frame Generation**.
- **Beautifully Formatted Settings** — No more endless single-column lists. Every game features **sectioned setting tables** that mirror in-game menus.
- **Historical FPS Graphs** — Track performance improvements over time across:
- Game Versions
- Proton / Native builds
- SteamOS Versions
- **Multi-Device Support** — Dedicated metrics and optimized settings for **Steam Deck OLED**, **Steam Deck LCD**, and **Steam Machine**, all on a single page with quick toggles.
- **Comprehensive Compatibility Tracking**
- Proton vs. Native status
- Online Play & Anti-Cheat status
- Straightforward "Playable" tags
- **Loading & Storage Metrics** — Track initial launch times, game load times, world loading, and **SD Card** performance.
- **Community & Steam Integration** — On-site comments plus existing reviews, artwork, and stats pulled directly from Steam.
- **Open Source** — Fully transparent. Help us build the best resource for Deck gamers.
## Tech Stack
- **[Next.js 16](https://nextjs.org)** — React framework (App Router)
- **[React 19](https://react.dev)** — UI library
- **[TypeScript](https://www.typescriptlang.org)** — Type safety
- **[Tailwind CSS v4](https://tailwindcss.com)** — Utility-first styling
- **[Elysia](https://elysiajs.com)** — Backend API framework
- **[Drizzle ORM](https://orm.drizzle.team)** — Type-safe database queries
- **[PostgreSQL](https://www.postgresql.org)** — Primary database
- **[better-auth](https://better-auth.com)** — Authentication (Google & Discord OAuth)
- **[Tiptap](https://tiptap.dev)** — Rich text editor
- **[Motion](https://motion.dev)** — Animations
- **[AWS S3](https://aws.amazon.com/s3/)** — File storage
- **[Bun](https://bun.sh)** — Package manager & runtime
## Getting Started
Clone the repository and install dependencies:
```bash
bun install
```
Copy the environment file and configure your values:
```bash
cp .env.example .env.local
```
Run the development server:
```bash
bun run dev
```
Open [http://localhost:3000](http://localhost:3000) to see the result.
## Scripts
| Command | Description |
|---------|-------------|
| `bun run dev` | Start development server (HTTPS) |
| `bun run build` | Create production build |
| `bun run start` | Start production server |
| `bun run lint` | Run ESLint |
## Contributing
We welcome contributions of all kinds! Whether you want to:
- Suggest a new feature
- Report a bug
- Improve the design
- Contribute code
Feel free to open an [issue](https://github.com/AdrianBonpin/deckyvault/issues) or submit a [pull request](https://github.com/AdrianBonpin/deckyvault/pulls).
If you're interested in helping develop or design the project, don't hesitate to reach out!
## Roadmap
This project is actively being developed. Here are some of the major items on our radar:
- [ ] Core database and API for game entries
- [ ] Advanced search and filter system
- [ ] Community submission and moderation tools
- [ ] FPS graphing and historical data visualization
- [ ] Steam API integration for reviews, artwork, and stats
- [ ] Support for additional handheld devices *(future consideration)*
Stay tuned for updates!
## License
This project is open-source. *(Add your specific license here, e.g., MIT, GPL-3.0)*
## Acknowledgements
- Built with love for the Steam Deck community.
- Check out the author's existing project for an idea of what to expect: **[https://grounds.ph](https://grounds.ph)**
---
*DeckyVault is an independent project and is not affiliated with Valve Corporation.*
```
- [ ] **Step 2: Verify no build impact**
Run: `bun run build`
Expected: Build succeeds (README changes don't affect build).
---
## Chunk 4: Verification & Lint/Build Cleanup
### Task 8: Final Verification
**Files:**
- All files created/modified in Tasks 1-7
- [ ] **Step 1: Run full lint**
Run: `bun run lint`
Expected: No errors or warnings.
- [ ] **Step 2: Run full build**
Run: `bun run build`
Expected: Build succeeds. Verify the following routes are generated:
- `/` — main page
- `/icon.png` — favicon (already existed)
- `/opengraph-image` — OG banner
- `/twitter-image` — Twitter card
- `/sitemap.xml` — sitemap
- `/robots.txt` — robots
- `/manifest.webmanifest` — PWA manifest
- [ ] **Step 3: Fix any lint or build errors**
If lint or build fails, fix the issues before claiming completion. Common issues to watch for:
- TypeScript errors in OG image files (ensure `readFile` import works)
- ESLint warnings on unused variables or missing dependencies
- Build failures from incorrect `ImageResponse` JSX
- [ ] **Step 4: Commit all changes**
```bash
git add app/layout.tsx app/opengraph-image.tsx app/twitter-image.tsx app/sitemap.ts app/robots.ts app/manifest.ts README.md
git commit -m "feat: add comprehensive SEO (metadata, OG images, sitemap, robots, manifest) and update README"
```
---
## Summary of Routes After Implementation
| Route | Type | Source File |
|-------|------|-------------|
| `/` | Static page | `app/page.tsx` |
| `/icon.png` | Favicon | `app/icon.png` |
| `/opengraph-image` | OG banner (PNG) | `app/opengraph-image.tsx` |
| `/twitter-image` | Twitter card (PNG) | `app/twitter-image.tsx` |
| `/sitemap.xml` | Sitemap | `app/sitemap.ts` |
| `/robots.txt` | Robots | `app/robots.ts` |
| `/manifest.webmanifest` | PWA manifest | `app/manifest.ts` |
+2 -1
View File
@@ -3,7 +3,7 @@
"version": "2026.0.1", "version": "2026.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev --experimental-https",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "eslint" "lint": "eslint"
@@ -19,6 +19,7 @@
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"drizzle-orm": "^0.45.2", "drizzle-orm": "^0.45.2",
"elysia": "^1.4.28", "elysia": "^1.4.28",
"lucide-react": "^1.11.0",
"motion": "^12.38.0", "motion": "^12.38.0",
"next": "16.2.4", "next": "16.2.4",
"pg": "^8.20.0", "pg": "^8.20.0",
-1
View File
@@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

-1
View File
@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

-1
View File
@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 128 B

-1
View File
@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

Before

Width:  |  Height:  |  Size: 385 B