diff --git a/apps/web/app/__tests__/sitemap.test.ts b/apps/web/app/__tests__/sitemap.test.ts index f5df060..e0e87a9 100644 --- a/apps/web/app/__tests__/sitemap.test.ts +++ b/apps/web/app/__tests__/sitemap.test.ts @@ -159,12 +159,12 @@ describe("Sitemap Generator (app/sitemap.ts)", () => { // ── sitemap({ id: 'static' }) ────────────────────────────────────── describe("sitemap({ id: 'static' })", () => { - it("returns 7 static page entries", async () => { + it("returns 8 static page entries", async () => { const mod = await import("@/app/sitemap") const result = await mod.default({ id: Promise.resolve("static") }) expect(Array.isArray(result)).toBe(true) - expect(result).toHaveLength(7) + expect(result).toHaveLength(8) }) it("first entry is homepage with priority 1.0", async () => { @@ -182,6 +182,7 @@ describe("Sitemap Generator (app/sitemap.ts)", () => { const urls = result.map((e: { url: string }) => e.url) expect(urls).toContain("https://deckyvault.xyz/games") expect(urls).toContain("https://deckyvault.xyz/compare") + expect(urls).toContain("https://deckyvault.xyz/plugin") }) it("includes lastModified on all entries", async () => { @@ -360,7 +361,7 @@ describe("Sitemap Generator (app/sitemap.ts)", () => { const mod = await import("@/app/sitemap") const result = await mod.default({ id: Promise.resolve("static") }) - expect(result).toHaveLength(7) + expect(result).toHaveLength(8) }) it("unknown id returns empty array", async () => { diff --git a/apps/web/app/game/[id]/game-page-client.tsx b/apps/web/app/game/[id]/game-page-client.tsx index 82d63e2..941df4a 100644 --- a/apps/web/app/game/[id]/game-page-client.tsx +++ b/apps/web/app/game/[id]/game-page-client.tsx @@ -18,6 +18,7 @@ import { GaugeIcon, ChevronDownIcon, PencilIcon, + X as XIcon, } from "lucide-react" import Link from "next/link" import { useSession } from "@/lib/auth-client" @@ -641,6 +642,23 @@ export function GamePageClient({ ) const presetsRef = useRef(null) + // DeckyVault plugin CTA dismissal (persisted across games in localStorage) + const [showPluginCta, setShowPluginCta] = useState(false) + useEffect(() => { + if (typeof window === "undefined") return + if (localStorage.getItem("dv-plugin-cta-dismissed") !== "1") { + setShowPluginCta(true) + } + }, []) + const dismissPluginCta = () => { + setShowPluginCta(false) + try { + localStorage.setItem("dv-plugin-cta-dismissed", "1") + } catch { + // ignore + } + } + // On mount, auto-open preset from URL useEffect(() => { const presetId = searchParams.get("preset") @@ -1425,6 +1443,39 @@ export function GamePageClient({ className='px-4 md:px-[10svw]' >
+ {showPluginCta && ( +
+
+
+ +
+
+

+ Have a Steam Deck? Record benchmarks automatically with the DeckyVault Decky plugin — one tap, no manual screenshots. +

+
+
+
+ + Get the plugin + + + +
+
+ )}

Performance Entries diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 3db983a..e458e6b 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -90,7 +90,7 @@ export default function RootLayout({ return ( {/* DNS prefetch + preconnect for external image CDNs */} diff --git a/apps/web/app/plugin/page-client.tsx b/apps/web/app/plugin/page-client.tsx index ade9a4d..03047d2 100644 --- a/apps/web/app/plugin/page-client.tsx +++ b/apps/web/app/plugin/page-client.tsx @@ -61,7 +61,7 @@ const SCREENSHOTS: Shot[] = [ title: "Record while you play", caption: "Once in-game, open the panel and hit Start Recording. A live timer tracks your session. Stop when you're done benchmarking.", - file: "recording.png", + file: "recording.jpg", photo: true, }, { @@ -69,7 +69,7 @@ const SCREENSHOTS: Shot[] = [ title: "Review & submit", caption: "After stopping, review the captured FPS, 1% lows, and power draw. Add notes, then upload straight to DeckyVault — or export to a file.", - file: "session-form.png", + file: "session-form.jpg", photo: true, }, { @@ -87,8 +87,8 @@ const HAS_SCREENSHOT: Record = { "panel-overview": true, "pair-qr": true, "launch-option": true, - recording: false, - "session-form": false, + recording: true, + "session-form": true, "entry-live": true, } @@ -184,6 +184,40 @@ const STEPS = [ }, ] +// ── Screenshot acts ──────────────────────────────────── +// The "See it in action" gallery is structured as a 3-act narrative so the +// story flows: Set up → Capture → Share. Each act has a heading + a list of +// shots. The last act's shot is rendered larger as a visual payoff. +const ACTS: { + number: string + title: string + subtitle: string + shotIds: string[] + layout: "grid-3" | "grid-2" | "feature" +}[] = [ + { + number: "1", + title: "Get set up", + subtitle: "Three quick steps before your first benchmark.", + shotIds: ["panel-overview", "pair-qr", "launch-option"], + layout: "grid-3", + }, + { + number: "2", + title: "Capture performance", + subtitle: "Hit record, play, then review your stats.", + shotIds: ["recording", "session-form"], + layout: "grid-2", + }, + { + number: "3", + title: "Share & compare", + subtitle: "Your entry appears on the game's page for the community.", + shotIds: ["entry-live"], + layout: "feature", + }, +] + export function PluginPageClient() { const [copied, setCopied] = useState(false) @@ -365,61 +399,122 @@ export function PluginPageClient() {

- {/* ── Screenshots gallery ──────────────────────────────── */} -
+ {/* ── See it in action: 3-act narrative ──────────────────────── */} +
-

- See it in action -

-

- A walkthrough of the plugin, from pairing to publishing. -

+
+

+ See it in action +

+

+ A walkthrough of the plugin, from pairing to publishing. +

+
-
- {SCREENSHOTS.map((shot, i) => ( - -
- {HAS_SCREENSHOT[shot.id] ? ( - // eslint-disable-next-line @next/next/no-img-element - {shot.title} - ) : ( -
- - - /plugin/{shot.file} - - - {shot.photo ? "device photo" : "screenshot"} coming soon - +
+ {ACTS.map((act, actIdx) => { + const shots = act.shotIds + .map((id) => SCREENSHOTS.find((s) => s.id === id)) + .filter((s): s is (typeof SCREENSHOTS)[number] => Boolean(s)) + + return ( +
+ {/* Act heading */} + +
+ {act.number}
+
+

+ {act.title} +

+

+ {act.subtitle} +

+
+
+ + {/* Shot grid for this act */} +
+ {shots.map((shot) => { + const isFeature = act.layout === "feature" + return ( + +
+ {HAS_SCREENSHOT[shot.id] ? ( + // eslint-disable-next-line @next/next/no-img-element + {shot.title} + ) : ( +
+ + + {shot.photo ? "device photo" : "screenshot"} not yet captured + +
+ )} +
+
+
+ {shot.title} +
+

+ {shot.caption} +

+
+
+ ) + })} +
+ + {/* Flow arrow between acts */} + {actIdx < ACTS.length - 1 && ( + +
+ )}
-
-
- - {i + 1} - - {shot.title} -
-

- {shot.caption} -

-
- - ))} + ) + })}
diff --git a/apps/web/app/plugin/page.tsx b/apps/web/app/plugin/page.tsx index 9892ca4..e2050c9 100644 --- a/apps/web/app/plugin/page.tsx +++ b/apps/web/app/plugin/page.tsx @@ -4,50 +4,147 @@ import { PluginPageClient } from "./page-client" export const dynamic = "force-dynamic" +const CANONICAL = "https://deckyvault.xyz/plugin" +const TITLE = "DeckyVault Plugin — Record & Upload Steam Deck Benchmarks" +const DESCRIPTION = + "Install the DeckyVault Decky Loader plugin to capture FPS, frame times, and power draw with MangoHud, then upload performance entries straight to DeckyVault from your Steam Deck. Free, open source, controller-friendly." + export const metadata: Metadata = { - title: "DeckyVault Plugin — Record & Upload Steam Deck Benchmarks", - description: - "Install the DeckyVault Decky Loader plugin to capture FPS, frame times, and power draw with MangoHud, then upload performance entries straight to DeckyVault from your Steam Deck.", + title: TITLE, + description: DESCRIPTION, keywords: [ "decky loader plugin", + "deckyvault plugin", "steam deck plugin", + "steam deck benchmark tool", "mangohud", + "mangohud steam deck", + "steam deck performance logging", + "steam deck fps recorder", "steam deck benchmark", "performance logging", "deckyvault", + "steam deck tdp", + "steam deck frame times", + "decky plugin install", ], - alternates: { canonical: "https://deckyvault.xyz/plugin" }, + alternates: { canonical: CANONICAL }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + "max-image-preview": "large", + "max-snippet": -1, + "max-video-preview": -1, + }, + }, openGraph: { - title: "DeckyVault Plugin — Record & Upload Steam Deck Benchmarks", + title: TITLE, description: - "Capture FPS and power draw with MangoHud, then upload to DeckyVault straight from your Steam Deck.", - url: "https://deckyvault.xyz/plugin", + "Capture FPS and power draw with MangoHud, then upload to DeckyVault straight from your Steam Deck. QR-code pairing, one-tap upload, auto game detection.", + url: CANONICAL, siteName: "DeckyVault", type: "website", + locale: "en_US", + images: [ + { + url: "/opengraph-image", + width: 1200, + height: 630, + alt: "DeckyVault Plugin for Steam Deck", + }, + ], }, twitter: { card: "summary_large_image", - title: "DeckyVault Plugin", + title: "DeckyVault Plugin — Steam Deck Benchmark Recorder", description: - "Record Steam Deck performance and upload to DeckyVault with the Decky Loader plugin.", + "Record Steam Deck performance with MangoHud and upload to DeckyVault with the Decky Loader plugin. QR pairing, one-tap upload.", + images: ["/opengraph-image"], }, + category: "technology", + authors: [{ name: "DeckyVault", url: "https://deckyvault.xyz" }], + creator: "DeckyVault", + publisher: "DeckyVault", } +// ── Structured data ──────────────────────────────────────────────── +// SoftwareApplication describes the plugin itself; FAQPage captures the +// install/setup steps so search engines can surface rich results. + const jsonLd = { "@context": "https://schema.org", "@type": "SoftwareApplication", name: "DeckyVault Plugin", applicationCategory: "UtilitiesApplication", operatingSystem: "SteamOS", + softwareVersion: "1.0.0", offers: { "@type": "Offer", price: "0", priceCurrency: "USD" }, description: - "A Decky Loader plugin that records Steam Deck performance metrics with MangoHud and uploads them to DeckyVault.", - url: "https://deckyvault.xyz/plugin", + "A Decky Loader plugin that records Steam Deck performance metrics (FPS, frame times, power draw) with MangoHud and uploads them to DeckyVault.", + url: CANONICAL, + downloadUrl: + "https://github.com/AdrianBonpin/deckyvault/releases", + author: { + "@type": "Organization", + name: "DeckyVault", + url: "https://deckyvault.xyz", + }, + featureList: [ + "Capture FPS, frame times, and 1% lows with MangoHud", + "Measure CPU + GPU power draw with peripheral overhead", + "QR-code pairing with your DeckyVault account", + "One-tap upload of performance entries", + "Automatic game and Proton version detection", + "Controller-friendly Quick Access Menu UI", + ], + screenshot: "https://deckyvault.xyz/plugin/panel-overview.jpg", +} + +const faqLd = { + "@context": "https://schema.org", + "@type": "FAQPage", + mainEntity: [ + { + "@type": "Question", + name: "How do I install the DeckyVault plugin?", + acceptedAnswer: { + "@type": "Answer", + text: "Install Decky Loader on your Steam Deck, then either use 'Install Plugin from ZIP File' with the downloaded plugin ZIP, or 'Install Plugin from URL' with the direct download link from the GitHub release.", + }, + }, + { + "@type": "Question", + name: "Do I need to manually copy an API key?", + acceptedAnswer: { + "@type": "Answer", + text: "No. Open the plugin in the Quick Access Menu, tap 'Pair with Phone', and scan the QR code with your phone. Confirm on deckyvault.xyz and your account links automatically — the API key is created and saved for you.", + }, + }, + { + "@type": "Question", + name: "What does the plugin record?", + acceptedAnswer: { + "@type": "Answer", + text: "It captures FPS averages, frame times, 1% lows, and total power draw (CPU + GPU plus peripheral overhead) using MangoHud, along with the detected game, Steam App ID, and Proton version.", + }, + }, + { + "@type": "Question", + name: "Is the DeckyVault plugin free?", + acceptedAnswer: { + "@type": "Answer", + text: "Yes, the plugin is free and open source.", + }, + }, + ], } const breadcrumbs = buildBreadcrumbList([ { name: "Home", url: "https://deckyvault.xyz/" }, - { name: "Plugin", url: "https://deckyvault.xyz/plugin" }, + { name: "Plugin", url: CANONICAL }, ]) export default function PluginPage() { @@ -57,6 +154,10 @@ export default function PluginPage() { type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> +