**Depends On:**`docs/2026-05-05-sitemap-pwa-refinements-architectural-spec.md` (sitemap and PWA constraints — some decisions are superseded)
---
## 1. Problem Statement
Seven objectives converge on three architectural concerns: **performance at scale** (sitemap load time, S3 lifecycle), **user expression** (profile photos, data dashboards), and **UX density** (game details layout, search filter polish, changelog hygiene). One objective (advanced search/filtering) is an audit revealing the feature was already fully shipped in v2026.0.95.
---
## 2. Objective 1 — Static Sitemaps
### 2.1 Observed Symptom
> "Sitemap takes too long to load"
The current sitemap (`app/sitemap.ts`) uses `force-dynamic` to generate a fresh sitemap from PostgreSQL on every request. With ~50K games, the DB round-trip + serialization adds latency. While the spec at `docs/2026-05-05-sitemap-pwa-refinements-architectural-spec.md` correctly removed ISR caching (which was poisoning Googlebot with empty responses), the pendulum swung too far toward liveness at the cost of response time.
### 2.2 Root Cause
`force-dynamic` forces a live DB query per request. For a resource that Googlebot fetches daily and that changes gradually (a few hundred games added per sync cycle), this is over-engineered for freshness and under-engineered for speed.
**Strategy**: Generate sitemap XML files **at build time** as a `postbuild` script, output them to the `public/` directory, and serve them as static files via Next.js. Use a **sitemap index** to split large catalogs into paginated child sitemaps (Google limit: 50,000 URLs or 50MB per file).
Where `sitemap-static.xml` contains the static pages (/, /games, /devices, /updates, /contact) and each `sitemap-games-N.xml` contains a page of game detail URLs.
#### 2.3.3 Build Script Location
**New file**: `scripts/build-sitemap.ts`
- Runs as `bun run scripts/build-sitemap.ts` in the `postbuild` npm script
- Uses the same Drizzle DB connection as the app
- Must NOT import Next.js — pure Node/bun script with direct DB access
- Reads `NEXT_PUBLIC_SITE_URL` from environment
**package.json change**:
```json
"scripts":{
"build":"next build --webpack",
"postbuild":"bun run scripts/build-sitemap.ts"
}
```
#### 2.3.4 Route Handling
**`app/sitemap.ts` changes**:
- Remove `force-dynamic` — this becomes a static route
- Redirect `/sitemap.xml` to the static file: return an empty sitemap that Next.js ignores, OR
- **Better approach**: Delete `app/sitemap.ts` entirely and rely on `public/sitemap.xml` being served as a static asset. Next.js serves `public/` files at the root path. `/sitemap.xml` → `public/sitemap.xml`.
**Verification**: `public/sitemap.xml` takes precedence over `app/sitemap.ts` in Next.js. Delete `app/sitemap.ts` to avoid conflicts.
#### 2.3.5 Staleness Mitigation
**Constraint**: The sitemap is generated at build time. If games are added between builds, they won't appear in the sitemap until the next deployment.
**Mitigation options** (choose one during implementation planning):
| Option | Pros | Cons |
|---|---|---|
| **A. Rebuild on every deployment** (default) | Simple, uses existing deploy pipeline | Sitemap lags behind live data |
| **B. Scheduled regeneration via cron** | Keeps sitemap fresh without full deploy | Requires Vercel Cron or cron-job service; adds moving part |
| **C. Hybrid: build-time static pages + runtime dynamic game entries** | Static pages are fast; game entries stay fresh | Most complex; two generation mechanisms |
**Recommendation**: Option A for now. Deployment frequency is high enough (multiple per week per changelog history) that staleness is acceptable. If Google Search Console reports indexing gaps, graduate to Option B.
#### 2.3.6 Security & Rate Limiting
- Sitemap files are public static assets — no auth, no rate limiting
- No sensitive data in sitemaps (only URLs and modification dates)
- DB credentials in the build script: use the same env vars as the app (`DATABASE_URL`), already available at build time
### 2.4 Validation Checklist
- [ ]`app/sitemap.ts` is deleted
- [ ]`scripts/build-sitemap.ts` exists and runs without Next.js imports
- [ ]`public/sitemap.xml` is a valid sitemap index
- [ ]`public/sitemap-games-*.xml` files contain valid game URLs
- [ ]`postbuild` script runs sitemap generation
- [ ] Total XML output < 50MB per file, < 50,000 URLs per file
- [ ]`https://deckyvault.xyz/sitemap.xml` serves instantly (no DB latency)
- [ ] Existing `lib/sitemap/` directory is fully removed (deprecated in v2026.0.97)
---
## 3. Objective 2 — Data Visualization Dashboard
### 3.1 Feature Scope
A new **public-facing dashboard page** (`/dashboard`) with three visualization panels:
| Panel | Data Source | Visualization |
|---|---|---|
| **Trending / Hot Games This Week** | Games with most benchmark submissions in last 7 days | Ranked list with sparkline charts |
| **Best Performing New Releases** | Games released in last 30 days, ranked by avg FPS | Sortable table with FPS bars |
| **Most Tested / Most Reported Games** | Top games by benchmark count + top games by report count | Dual-column leaderboard |
### 3.2 Architecture
#### 3.2.1 New Page Route
**File**: `app/dashboard/page.tsx`
- Public route — no auth required
- SSR with `force-dynamic` (data is computed fresh per request)
- Parallel data fetching for three panels
#### 3.2.2 New API Endpoints
**File**: `lib/api/dashboard-public.ts` (separate from existing `dashboard.ts` which is admin-only)
**Change**: `useState(true)` — expanded by default.
**Rationale**: System requirements are critical for Steam Deck users deciding if a game can run. Hiding them behind a click reduces information density on a page where users are actively researching performance.
#### Change 2: Move Details Inline with Game Info
**Current layout (Section 2)**:
```
┌────────────────────────────┬──────────────┐
│ About (description) │ Details │
│ │ (meta grid) │
│ System Reqs (collapsed) │ Platforms │
│ │ Platform Sup │
└────────────────────────────┴──────────────┘
```
**Target layout (Section 2 revised)**:
```
┌───────────────────────────────────────────────┐
│ Game Info (inline) │
│ ┌──────────┬──────────┬──────────┬──────────┐ │
│ │ Developer│ Publisher│ Source │ Release │ │
│ │ Platform │ Multiplay│ Price │ Metacrit │ │
│ └──────────┴──────────┴──────────┴──────────┘ │
│ │
│ Platform Support (inline badges, not dropdown)│
│ │
├───────────────────────────────────────────────┤
│ System Requirements (EXPANDED BY DEFAULT) │
│ ┌──────────────────┬──────────────────────┐ │
│ │ Minimum │ Recommended │ │
│ └──────────────────┴──────────────────────┘ │
├───────────────────────────────────────────────┤
│ Description │
└───────────────────────────────────────────────┘
```
**Key changes**:
- Move metadata grid ABOVE description (currently to the right)
- Make platform support an inline badge row (not a stacked list)
- System requirements always visible, at full width, with both min/recommended side by side
- Description pushed down as secondary content
#### Change 3: Controller and Mobile Optimization
**Controller (gamepad)**:
- The `useGamepadNavigation` hook is already imported and used on the games listing page. Extend to game detail page.
- Gamepad focus ring on preset cards, filter selects, comment section
- L1/R1: tab between "Presets" and "Comments" sections (scroll to anchor)
- Y button: toggle system requirements (even when expanded by default)
- X button: "Add Benchmark" quick action
**Mobile viewing**:
- Stack the two-column layout (description + details) into single column on mobile
- Preset cards: horizontal scroll with snap points (already implemented via flex overflow)
- Ensure 44×44px touch targets on all interactive elements (filter selects, expand buttons, external links)
- Viewport optimization already applied (viewport-fit=cover from v2026.0.97)
The advanced search and filtering system described in the objective was **fully implemented in v2026.0.95** and refined in v2026.0.97. A line-by-line audit confirms every requested capability exists:
| Requested Feature | Implementation | Status | File |
**Minor UX gap**: When loading a saved filter with multiple genres, the chips display correctly but there's no visual indication of which genres were auto-selected from the saved filter vs manually clicked. This is below the threshold for a dedicated objective — address as part of saved filters loading interaction if desired.
### 6.3 Recommendation
**No implementation work required for this objective.** Mark as verified/complete. If the user observes specific filter behavior they find incorrect, that's a bug report, not a feature request.
-`getBucketStats(): Promise<{ objectCount: number, totalSizeBytes: number }>`
**Design principle**: All S3 interactions go through this module. No direct `S3Client` usage in API routes or scripts.
#### 7.2.2 Daily Cron Job (Unified)
**Design**: A single daily cron endpoint (`/api/cron/daily`) that handles all recurring daily maintenance tasks. This avoids endpoint fragmentation — new daily jobs are added to this same handler in the future.
// 1. Query all referenced avatar keys from user.image column
// 2. List all objects in avatars/ prefix
// 3. Delete any S3 object whose key is NOT referenced by a user
// 4. Log results (structured JSON)
results.s3Cleanup={deleted: count}
// Future daily jobs go here ──────────────────────────────────
// results.someOtherJob = { ... }
console.info(JSON.stringify({
event:"cron_daily_completed",
...results,
timestamp: newDate().toISOString(),
}))
returnResponse.json(results)
}
```
**vercel.json** (or Vercel dashboard):
```json
{
"crons":[
{
"path":"/api/cron/daily",
"schedule":"0 3 * * *"
}
]
}
```
Schedule: 3:00 AM UTC daily (low-traffic window).
**CRON_SECRET**: New env var — a random 64-char string used to authenticate cron requests.
#### 7.2.3 S3 Lifecycle Policy (AWS-Side)
**Configure on the S3 bucket itself** (not in application code):
-`avatars/`: No auto-deletion (avatars are persistent)
-`temp/`: Expire after 24 hours (for temporary uploads if needed later)
- Enable Intelligent-Tiering for cost optimization on `avatars/`
#### 7.2.4 Bucket Inventory & Monitoring
**New API endpoint** (admin-only, for the manage dashboard):
```
GET /api/admin/storage/stats
Auth: Admin required
Returns:
{
bucket: "deckyvault-uploads",
objectCount: 1234,
totalSizeBytes: 52428800,
avatarCount: 567,
orphanedCount: 3,
lastCleanupAt: "2026-05-09T03:00:00Z"
}
```
Add a storage card to the manage dashboard (`components/manage/` or the dashboard page).
### 7.3 Security
- **Cron endpoint** (`/api/cron/daily`): Protected by `CRON_SECRET` bearer token. Rejects all requests without it. Single endpoint for all daily maintenance jobs.
- **Admin storage endpoint**: Protected by `requireAdmin` auth guard.
- **S3 bucket**: Server-side IAM role with limited permissions (PutObject, DeleteObject, ListObjects on specific prefixes only). No wildcard permissions.
- **Public access**: Only `avatars/` prefix is publicly readable. All other prefixes are private.
- **No user-accessible S3 endpoints**: Users never interact with S3 directly. All uploads go through the server.
### 7.4 No Existing Cron — Explicit Decision
A search of the codebase confirms there is **no existing cron job mechanism** — no `vercel.json` crons config, no cron routes, no scheduled tasks. The cron infrastructure is created fresh in this objective.
---
## 8. Objective 7 — Changelog & Version Bump
### 8.1 Procedure
Follow the established pattern from CHANGELOG.md (Keep a Changelog format) and content/updates/ (frontmatter + markdown).
summary:"New public data dashboard, profile photo uploads, faster sitemaps, game details UX improvements, and S3 storage infrastructure."
---
# Hello everyone!
A feature-packed update with several quality-of-life improvements and new tools for the community.
## Data Dashboard
Head over to the new [Dashboard](https://deckyvault.xyz/dashboard) to see what's trending this week, which new releases are performing best on Steam Deck, and which games the community is testing most actively.
## Profile Photos
You can now upload a profile photo! Visit your [Profile Settings](https://deckyvault.xyz/profile) and drag-and-drop an image to personalize your account.
## Game Details Improvements
System requirements are now shown upfront on game detail pages — no more clicking to expand. The page layout has been streamlined with metadata inline with game info, and gamepad navigation works across the entire page.
## Behind the Scenes
- Sitemaps are now pre-generated at build time for faster crawling
- S3 storage infrastructure for uploads with daily automated cleanup
- Mobile-optimized game detail pages
As always, if you run into any issues, feel free to visit the [contact page](https://deckyvault.xyz/contact) to submit a report!
> Adrian Bonpin
```
**Date replacement**: Replace `2026-05-XX` with the actual ship date in both the CHANGELOG and the update `.md` file.
### 8.2 Version Bump Constraint
**Not a major version change**: The version scheme uses `YYYY.MINOR.PATCH` format (e.g., `2026.0.98`). The user explicitly stated "not major yet" — bump the patch from `.971` → `.98`.
---
## 9. Architecture Boundaries — Cross-Cutting
### 9.1 Performance Budget
| Metric | Budget | Rationale |
|---|---|---|
| Static sitemap generation | < 30s | Build step; must not block deployment |
| Game details page LCP | < 2.5s | Presets + charts load after initial render |
### 9.2 Dependency Additions
| Package | Purpose | Version Constraint |
|---|---|---|
| `sharp` | Server-side image resizing for avatars | Already in trustedDependencies; ensure in `dependencies` |
No other new dependencies. Charts use existing `echarts-for-react`. S3 uses existing `@aws-sdk/client-s3`. File uploads use native `Request.formData()`.
| R1 | Static sitemap grows stale between deployments | Medium | Low | Current deploy cadence is 2-4x per week. Acceptable. Add scheduled rebuild only if needed. |
| R2 | Sitemap with >50K games exceeds single-file limit | Low | Medium | Pagination built into the generator from day one (45K URLs per file). |
| R3 | Sharp fails to install on Vercel (native dependency) | Medium | Medium | Sharp is already in `trustedDependencies`. Test in preview deploy. Fallback: skip resize, upload original (with size validation). |
| R4 | S3 cron accidentally deletes active avatars | Low | High | Cron compares against live `user.image` column values. Only deletes objects with zero references. Thoroughly tested with dry-run mode first. |
| R5 | Dashboard queries cause DB load at peak times | Low | Low | Queries target small subsets (7-day window, 30-day window with HAVING >= 3). Use existing DB indexes. |
| R6 | Game details layout change breaks existing user muscle memory | Medium | Low | Keep all existing data visible. Only reorder, not remove. System reqs expanded = more information, not less. |
| R7 | Vercel Cron is not available on the current plan | Low | Medium | Vercel Hobby has 1 cron job with daily frequency. If on Pro, 2 crons. Fallback: GitHub Actions scheduled workflow calling the cron endpoint. |
---
## 11. Out of Scope (Explicitly)
- **Sitemap real-time updates**: Static generation only. No hybrid or event-driven sitemap updates.
- **Dashboard personalization**: Public dashboard is global, not per-user. No "my dashboard" yet.
- **Multiple avatar sizes**: Single 256×256 size. No responsive srcSet.
- **Avatar crop UI**: Center-crop only. No user-customizable crop region.
- **Game detail page full redesign**: Layout changes only. No new data, no removed features.
- **New advanced filter dimensions**: The 12 existing filter dimensions are complete. No new filters.
- **S3 bucket migration**: Using existing or new bucket — no data migration from other storage.
- **User-uploaded game screenshots**: Avatars only. Game media stays on Steam CDN/SteamGridDB.
- **Major version bump**: Explicitly "not major yet."
---
## 12. Validation Checklist (Post-Implementation)
### Sitemap
- [ ]`app/sitemap.ts` no longer exists
- [ ]`https://deckyvault.xyz/sitemap.xml` returns valid sitemap index XML
- [ ] Each child sitemap has ≤ 45,000 URLs and < 50MB
- [ ] Response time is < 50ms (static file serve, no DB query)
- [ ] Google Search Console accepts and processes the sitemap
- [ ] Sitemap regenerates on every `bun run build`
### Dashboard
- [ ]`/dashboard` page loads with all three panels
- [ ] Trending panel shows games with recent submissions
- [ ] New releases panel shows games from last 30 days with ≥ 3 benchmarks
- [ ] Most tested/most reported panels show correct counts
- [ ] Dashboard accessible from navbar
- [ ] Dashboard included in static sitemap entries
### Profile Photos
- [ ] Avatar upload accepts JPG, PNG, WebP files
- [ ] Files > 5MB are rejected
- [ ] Non-image files are rejected (magic byte check)
- [ ] Uploaded avatar appears in profile header
- [ ] Uploaded avatar appears in settings page preview
- [ ] S3 bucket contains the uploaded avatar webp
### Game Details
- [ ] System requirements are visible without clicking
- [ ] Metadata grid appears above description
- [ ] Platform support is inline badges, not stacked list
- [ ] Gamepad D-pad navigates preset cards
- [ ] Gamepad L1/R1 switches between sections
- [ ] Mobile layout (≤ 768px) stacks to single column
### S3 Storage
- [ ] Cron endpoint (`/api/cron/daily`) is protected by `CRON_SECRET`
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.