diff --git a/docs/superpowers/specs/2026-04-27-community-presets-seo-design.md b/docs/superpowers/specs/2026-04-27-community-presets-seo-design.md new file mode 100644 index 0000000..afa3975 --- /dev/null +++ b/docs/superpowers/specs/2026-04-27-community-presets-seo-design.md @@ -0,0 +1,367 @@ +# Community Presets UX + SEO Overhaul Design Spec + +## Overview + +Two major improvements to DeckyVault: +1. **Community Presets UX Overhaul** — View settings, delete/report, horizontal layout, reorder above graphs +2. **SEO Robustness** — Dynamic metadata, sitemap expansion, JSON-LD, dynamic OG images for game pages + +--- + +## Area 1: Community Presets UX Overhaul + +### 1.1 Layout Change — Horizontal Scroll Row + +**Current:** 3-column vertical grid (`grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3`) + +**Proposed:** Single horizontal scroll row (`flex overflow-x-auto`) with cards ordered newest-first (left-to-right). Cards are date-sorted by `createdAt` descending, so the newest preset appears leftmost. + +Implementation: +- Use `flex overflow-x-auto gap-4 pb-4` on the container +- Each card is a `flex-shrink-0 w-72` (or similar fixed width) +- Hide scrollbar via CSS (`scrollbar-width: none` + `::-webkit-scrollbar { display: none }`) +- Add left/right scroll buttons (chevrons) overlaid at the edges for discoverability + +### 1.2 Section Reorder — Presets Above Statistics + +**Current:** Section order in `game-page-client.tsx`: +1. Hero Header +2. About + Details +3. Device Selector + Filters +4. Statistics Dashboard (charts) +5. Community Presets + +**Proposed:** +1. Hero Header +2. About + Details +3. Device Selector + Filters +4. **Community Presets** ← moved up +5. Statistics Dashboard ← moved down + +Simply reorder the two `` sections in the JSX. + +### 1.3 View Settings — AnimatePresence Modal + +**Current:** Preset cards show summary info (FPS, upvotes, tech tags, settings count) but the actual `settingsJson` content is not displayed anywhere. + +**Proposed:** Clicking a preset card opens a modal using `AnimatePresence` + `layoutId` for a smooth animated transition from the card to the expanded modal. The modal shows: + +- **Settings Table**: Each `GameSettingCategory` rendered as a collapsible section with category name as header, and settings as a two-column table (setting name → value) +- **Full Metadata**: Device name, Proton version, SteamOS version, FPS (avg/min/max), Upscaler, Frame Gen, Launch Options, User Notes +- **Community info**: Submitter (if we include userId in the preset data), upvotes/downvotes, verified badge, creation date +- **Actions**: Upvote/downvote buttons, Report button, Delete button (owner/admin only) + +Animation detail: +- Each card has a `layoutId={preset.id}` on the card container +- The modal wrapper uses the same `layoutId` when replacing the card +- `AnimatePresence mode="wait"` wraps the transition +- The card's click handler sets `selectedPresetId` state +- When `selectedPresetId` is set, render the modal overlay with `motion.div layoutId={selectedPresetId}` + +### 1.4 Delete — Soft Delete for Owners + Admins + +**Current API state:** +- Admin soft-delete via `DELETE /api/performance/:id` (sets `isRemoved: true`) +- Owner hard-delete via `DELETE /api/performance/:id/user-delete` (permanently removes row) + +**Proposed changes:** + +#### 1.4.1 Change user-delete to soft delete + +Modify the `/api/performance/:id/user-delete` endpoint to set `isRemoved: true` instead of hard-deleting. This aligns owner deletes with admin deletes. Add an optional `reason` body parameter that sets `removedReason`. + +#### 1.4.2 Delete UI in preset modal + +In the preset detail modal: +- **Owner sees**: A "Delete" button (red, secondary style). Clicking it shows a confirmation dialog: "Are you sure you want to remove this preset? This can be undone by an admin." On confirm, calls `DELETE /api/performance/:id/user-delete`. +- **Admin sees**: Same "Delete" button on any preset (not just their own). +- **Other users**: No delete button. + +### 1.5 Report Flow + +**New database table: `reports`** + +| Column | Type | Notes | +|---|---|---| +| id | text PK | UUID auto-generated | +| entryId | text FK → performance_entries.id | onDelete: cascade | +| reporterId | text FK → user.id | onDelete: cascade | +| reason | enum(inaccurate, spam, inappropriate, other) | Required | +| details | text | Optional free-text | +| status | enum(open, reviewed, dismissed) | Default: open | +| createdAt | timestamp | Default: now() | + +**New API route: `POST /api/performance/:id/report`** + +- Auth required (any user role) +- Body: `{ reason: "inaccurate" | "spam" | "inappropriate" | "other", details?: string }` +- Prevents duplicate reports (one report per user per entry) +- Returns the created report + +**Report UI in preset modal:** +- Every preset card/modal shows a "Report" icon button (flag icon) +- Clicking it opens a small popover/dropdown with reason selection +- On submit, calls the API and shows a toast "Report submitted. Thank you." +- If user already reported this entry, show "Already reported" instead + +### 1.6 Preset Data Enrichment + +The server component currently does not pass `userId`, `settingsJson` (full content), or `userNotes` to the client. To support the modal view: + +**Changes to `app/game/[id]/page.tsx` serialized presets:** + +Add these fields to the `serializedPresets` mapping: +- `settingsJson` — the full `GameSettingCategory[]` array +- `userNotes` — string | null +- `userId` — the owner's user ID (needed for delete permission check) +- `userName` — the owner's display name +- `userImage` — the owner's avatar URL +- `launchOptions` — string | null +- `downvotes` — number +- `verifiedAt` — string | null + +The SQL query in `page.tsx` needs to join on `user` table to get `userName` and `userImage`. + +### 1.7 Client-Side Preset Type Update + +Update the `Preset` interface in `game-page-client.tsx` to include the new fields: +```typescript +interface Preset { + id: string + hardwareSlug: string + hardwareName: string + upvotes: number + downvotes: number + settingsJson: GameSettingCategory[] | null + settingsCount: number + fpsAvg: number | null + fpsLow: number | null + fpsHigh: number | null + upscalerType: string | null + upscalerVersion: string | null + frameGenMethod: string | null + protonVersion: string | null + osVersion: string | null + launchOptions: string | null + userNotes: string | null + userId: string + userName: string | null + userImage: string | null + verifiedAt: string | null + createdAt: string +} +``` + +--- + +## Area 2: SEO Overhaul + +### 2.1 Game Details Page — Dynamic Metadata + +**Current:** `export const metadata = { title: "Game" }` — no game-specific data in metadata. + +**Proposed:** Replace with `generateMetadata` function: + +```typescript +export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise { + const { id } = await params + // ... resolve game (reuse existing logic) + return { + title: `${game.title} - Benchmarks & Settings`, + description: game.description + ? `${game.description.slice(0, 160)}` + : `Find benchmarks, community presets, and performance settings for ${game.title} on Steam Deck.`, + 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", + }, + 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`], + }, + } +} +``` + +### 2.2 Game Details Page — JSON-LD Structured Data + +Add a `