docs: add design spec for games list, contact page, and SEO updates

This commit is contained in:
2026-04-28 01:32:40 +08:00
parent f35b6aee1e
commit e50eef52e8
@@ -0,0 +1,168 @@
# Games List, Contact Page & SEO — Design Spec
**Date:** 2026-04-28
**Status:** Approved
---
## 1. Games List Page (`/games`)
### Architecture
Server component for initial render + SEO metadata, client component for interactive infinite scroll, search, and filtering.
### Data Flow
1. `page.tsx` (server) fetches first 24 games with total count via direct DB query
2. `games-page-client.tsx` (client) renders compact grid + search/filter bar
3. As user scrolls near bottom, `IntersectionObserver` triggers fetch to `/api/games?offset=N&limit=24&search=...&filter_...`
4. URL search params update reflectively: `?q=term&genre=Action&device=steam-deck&sort=benchmarks`
### Card Design (Compact)
Each card displays:
- Game cover image (capsule image, portrait 2:3 ratio, `next/image` with lazy loading)
- Title (12 lines, truncated with `line-clamp-2`)
- 12 compact stat badges:
- Benchmark count (icon + number)
- Avg FPS badge (if data exists) or "No data yet" muted text
- Deck status pill (Native/Proton/Unsupported) — from `gamePlatformSupport`
- On hover: subtle border highlight + scale, links to `/game/{id}`
### Filters
- **Search**: Debounced text input (300ms), uses `?search=` on `/api/games`
- **Genre**: Multi-select pill buttons derived from aggregating genres from initial game batch
- **Device**: Pill buttons matching existing devices page pattern (all hardware slugs)
- **Sort**: Dropdown — Name AZ, Most Benchmarks, Recently Added
### Performance
- Initial 24 items server-rendered for SEO crawlability
- Subsequent pages fetched client-side (24 per page)
- `IntersectionObserver` on sentinel element at bottom of grid
- `next/image` with `sizes` attribute for responsive srcsets
- Total count tracked to stop loading when exhausted
- Suspense boundary around grid for streaming initial data
### SEO
- `generateMetadata()`: title "Games — DeckyVault", description, OG tags, canonical `https://deckyvault.xyz/games`
- JSON-LD `ItemList` schema with initial 24 games
- Page is indexable (unlike `/search`)
### File Structure
```
app/games/
page.tsx — Server component: fetch initial games, metadata, JSON-LD
games-page-client.tsx — Client component: grid, infinite scroll, search, filters
```
---
## 2. Contact / Report Page (`/contact`)
### Architecture
Client component for the form UI, server-side Elysia API route for Discord webhook submission.
### Form Fields
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| Category | Radio/pill select | Yes | Bug Report, Game Data Issue, User Report, Feature Request, General Feedback, Database Error |
| Name | Text | No | Pre-filled from session if logged in |
| Email | Text | No | Pre-filled from session if logged in |
| Subject | Text | Yes | Max 200 chars |
| Message | Textarea | Yes | Max 2000 chars |
| Game URL | Text | Conditional | Shown only for "Game Data Issue" category. Validates `/game/` URL pattern |
| Honeypot | Hidden text | No | Must remain empty (anti-bot) |
| Timestamp | Hidden | No | Client-side submission start time (anti-bot timing check) |
### Discord Webhook Integration
New Elysia route: `POST /api/contact`
**Payload → Discord Embed mapping:**
- Category → embed color (🔴 red: Bug/DB Error, 🟡 yellow: Game Data, 🔵 blue: User Report, 🟢 green: Feature Request, ⚪ grey: General)
- Name, Email, Subject, Message → embed fields
- Game URL → link field (when present)
- User ID (if authenticated) → footer field
- IP hash (SHA-256 truncated) → footer field for tracking
**Environment variable:** `DISCORD_WEBHOOK_URL` added to `.env.example`
### Anti-Abuse Measures
1. **Honeypot field**: Hidden input that bots fill; server rejects if non-empty
2. **Timing check**: Server rejects submissions where timestamp < 3 seconds ago (bots submit instantly)
3. **IP rate limiting**: In-memory LRU cache, max 3 submissions per IP per hour
4. **Server-side validation**: All required fields, max lengths, category enum check, Game URL format validation
### UX
- Success: Clear success message after submission (inline, not toast)
- Error: Inline validation errors per field; rate limit exceeded shows friendly message asking to try later
- Accessible: Proper `<label>` associations, ARIA attributes, keyboard navigable, focus management
- No CAPTCHA — timing + honeypot + rate limit is sufficient
### SEO
- `robots: { index: false, follow: true }` — not useful indexed
- Simple metadata: title "Contact & Report — DeckyVault"
### File Structure
```
app/contact/
page.tsx — Client component: contact form with category picker, validation, submission
lib/api/contact.ts — Elysia route: POST handler, Discord webhook, rate limit, validation
```
---
## 3. SEO & Sitemap Updates
### Sitemap Additions
Current sitemap includes: `/`, `/devices`, `/game/{id}` (all), `/devices/{slug}` (all).
**Add:**
| URL | Priority | Change Frequency |
|-----|----------|-----------------|
| `/games` | 0.7 | weekly |
| `/contact` | 0.3 | monthly |
| `/login` | 0.3 | monthly |
| `/signup` | 0.3 | monthly |
**Enhancement:** Add `images` property to game sitemap entries using `capsuleImage` field.
### Robots.txt Updates
Add explicit disallow rules:
```
Disallow: /admin
Disallow: /api
```
This is in addition to the per-page `robots` metadata that already exists on admin and search pages.
### Files to Modify
```
app/sitemap.ts — Add static pages, enhance game entries with images
app/robots.ts — Add /admin and /api disallow rules
```
---
## Edge Cases & Security
- **Games list with zero results**: Show friendly empty state with icon + message
- **Games list API failure**: Client-side error state with retry CTA
- **Contact form rate limit exceeded**: HTTP 429 response, inline message "Please try again in a few minutes"
- **Discord webhook down**: Server catches webhook POST failure, returns 500 with generic error message (no Discord implementation details leaked)
- **XSS in form fields**: All fields validated server-side; Discord embed content is structured (not raw HTML)
- **Games grid empty DB**: Server component renders empty state, no client-side fallback needed
- **Deep pagination**: Cap offset at 10000 (to prevent DB strain from bots scrolling endlessly); show "You've reached the end" message