Files
deckyvault/app/sitemap.ts
T
adrianbonpin dcf6066b2b feat: sitemap remediation, PWA activation, search refinements, Steam Deck UX
Sitemap:
- Switch from ISR (revalidate=3600) to force-dynamic for per-request generation
- Flatten into single app/sitemap.ts with inlined static entries and DB queries
- NULL-safe syncStatus filter: ne(games.syncStatus, 'failed') OR isNull(games.syncStatus)
- Structured JSON logging for generated URLs and DB errors
- Delete lib/sitemap/* helpers and app/api/revalidate-sitemap route
- Add basic vitest coverage for sitemap exports

PWA & Offline:
- Add @serwist/next service worker (webpack build) with runtime caching
- Cache strategies: stale-while-revalidate for game pages, network-first for listings/API,
  cache-first for Steam CDN images
- Offline fallback page (public/offline.html)
- Manifest icons: 192px maskable + 512px any
- Viewport meta with viewport-fit=cover, user-scalable=no
- Apple mobile web app meta tags

Search / Filter Refinements:
- Multi-genre OR support in listing API (comma-separated genres)
- Device-scoped FPS filter (min/max FPS constrained to selected device)
- Client-side URL state sync via router.replace for shareable filtered views
- Initialize filter state from URL params on mount
- Auto-collapse filter panel on saved-filter load
- Fix multi-genre saved filter parsing (comma-separated)

Steam Deck / Touch / Gamepad UX:
- WCAG 2.1 AA touch targets (44x44px) on all filter controls
- .gamepad-focus CSS focus ring for controller navigation
- useGamepadNavigation hook: D-pad/left-stick roving tabindex, A/B/X/Y actions
- Integrate gamepad hook into games page (X=search, Y=toggle filters)

Chore:
- Bump version to 2026.0.97
2026-05-05 16:20:28 +08:00

115 lines
3.1 KiB
TypeScript

import type { MetadataRoute } from "next"
import { db } from "@/lib/db/index"
import { games, hardware } from "@/lib/db/schema"
import { or, ne, isNull } from "drizzle-orm"
export const dynamic = "force-dynamic"
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://deckyvault.xyz"
function buildStaticEntries(): MetadataRoute.Sitemap {
return [
{
url: BASE_URL,
lastModified: new Date(),
changeFrequency: "weekly" as const,
priority: 1,
},
{
url: `${BASE_URL}/games`,
lastModified: new Date(),
changeFrequency: "daily" as const,
priority: 0.8,
},
{
url: `${BASE_URL}/devices`,
lastModified: new Date(),
changeFrequency: "monthly" as const,
priority: 0.6,
},
{
url: `${BASE_URL}/updates`,
lastModified: new Date(),
changeFrequency: "weekly" as const,
priority: 0.5,
},
{
url: `${BASE_URL}/contact`,
lastModified: new Date(),
changeFrequency: "yearly" as const,
priority: 0.3,
},
]
}
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const staticEntries = buildStaticEntries()
try {
const [gameRows, deviceRows] = await Promise.all([
db
.select({
id: games.id,
updatedAt: games.updatedAt,
capsuleImage: games.capsuleImage,
})
.from(games)
.where(or(ne(games.syncStatus, "failed"), isNull(games.syncStatus))),
db
.select({
slug: hardware.slug,
createdAt: hardware.createdAt,
})
.from(hardware),
])
const gameEntries: MetadataRoute.Sitemap = gameRows.map((row) => {
const image =
row.capsuleImage &&
typeof row.capsuleImage === "string" &&
row.capsuleImage.trim().startsWith("https://") &&
row.capsuleImage.trim().length <= 2048
? row.capsuleImage.trim()
: undefined
return {
url: `${BASE_URL}/game/${row.id}`,
lastModified: row.updatedAt ?? undefined,
changeFrequency: "weekly" as const,
priority: 0.7,
...(image ? { images: [image] } : {}),
}
})
const deviceEntries: MetadataRoute.Sitemap = deviceRows.map((row) => ({
url: `${BASE_URL}/devices/${row.slug}`,
lastModified: row.createdAt ?? undefined,
changeFrequency: "monthly" as const,
priority: 0.5,
}))
console.info(
JSON.stringify({
event: "sitemap_generated",
gameCount: gameEntries.length,
deviceCount: deviceEntries.length,
staticCount: staticEntries.length,
totalUrls: staticEntries.length + gameEntries.length + deviceEntries.length,
generatedAt: new Date().toISOString(),
}),
)
return [...staticEntries, ...gameEntries, ...deviceEntries]
} catch (err) {
console.error(
JSON.stringify({
event: "sitemap_db_error",
error: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
generatedAt: new Date().toISOString(),
}),
)
return staticEntries
}
}