diff --git a/website/public/promo/og.png b/website/public/promo/og.png index 9405e15023..cfeb960f63 100644 Binary files a/website/public/promo/og.png and b/website/public/promo/og.png differ diff --git a/website/src/app/(v2)/(blog)/changelog/page.tsx b/website/src/app/(v2)/(blog)/changelog/page.tsx index 292114040b..48ae689663 100644 --- a/website/src/app/(v2)/(blog)/changelog/page.tsx +++ b/website/src/app/(v2)/(blog)/changelog/page.tsx @@ -136,14 +136,14 @@ export default async function BlogPage() { return ( <> -
+
{/* Background Grid */}
-

+

Changelog

diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/ObservabilitySection.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/ObservabilitySection.tsx index 4914d641e8..742c181e8a 100644 --- a/website/src/app/(v2)/(marketing)/(index)/sections/ObservabilitySection.tsx +++ b/website/src/app/(v2)/(marketing)/(index)/sections/ObservabilitySection.tsx @@ -47,8 +47,8 @@ export const ObservabilitySection = () => {
{/* Top Shine Highlight */} @@ -72,12 +72,14 @@ export const ObservabilitySection = () => { priority quality={90} /> + {/* Linear gradient overlay - darker on bottom */} +
{/* Text content overlapping bottom */} -
+
{ ]; return ( -
+
{tier.name === "Enterprise" ? "Contact" @@ -489,11 +489,11 @@ export default function PricingPageClient() { ]; return ( -
+
-

+

Rivet {activeTab === "cloud" ? "Cloud Pricing" : "Self-Host"}

@@ -558,7 +558,7 @@ export default function PricingPageClient() {

Get Started @@ -587,7 +587,7 @@ export default function PricingPageClient() {
Contact Sales diff --git a/website/src/app/(v2)/(marketing)/solutions/game-servers/page.tsx b/website/src/app/(v2)/(marketing)/solutions/game-servers/page.tsx new file mode 100644 index 0000000000..935fbfed0a --- /dev/null +++ b/website/src/app/(v2)/(marketing)/solutions/game-servers/page.tsx @@ -0,0 +1,816 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { + Terminal, + Zap, + Globe, + ArrowRight, + Box, + Database, + Check, + Cpu, + RefreshCw, + Clock, + Cloud, + LayoutGrid, + Activity, + Wifi, + AlertCircle, + Gamepad2, + MessageSquare, + Bot, + Users, + FileText, + Workflow, + Gauge, + Eye, + Play, + Calendar, + GitBranch, + Timer, + Mail, + CreditCard, + Bell, + Server, + Sword, + Trophy, + Target, +} from "lucide-react"; +import { motion } from "framer-motion"; + +// --- Shared Design Components --- +const Badge = ({ text, color = "red" }) => { + const colorClasses = { + orange: "text-orange-400 border-orange-500/20 bg-orange-500/10", + blue: "text-blue-400 border-blue-500/20 bg-blue-500/10", + purple: "text-purple-400 border-purple-500/20 bg-purple-500/10", + emerald: "text-emerald-400 border-emerald-500/20 bg-emerald-500/10", + red: "text-red-400 border-red-500/20 bg-red-500/10", + }; + + return ( +
+ + {text} +
+ ); +}; + +const CodeBlock = ({ code, fileName = "match.ts" }) => { + return ( +
+
+
+
+
+
+
+
+
{fileName}
+
+
+
+					
+						{code.split("\n").map((line, i) => (
+							
+ + {i + 1} + + + {(() => { + // Simple custom tokenizer for this snippet + const tokens = []; + let current = line; + + // Handle comments first (consume rest of line) + const commentIndex = current.indexOf("//"); + let comment = ""; + if (commentIndex !== -1) { + comment = current.slice(commentIndex); + current = current.slice(0, commentIndex); + } + + // Split remaining code by delimiters but keep them + const parts = current + .split(/([a-zA-Z0-9_$]+|"[^"]*"|'[^']*'|\s+|[(){},.;:[\]])/g) + .filter(Boolean); + + parts.forEach((part, j) => { + const trimmed = part.trim(); + + // Keywords + if ( + [ + "import", + "from", + "export", + "const", + "return", + "async", + "await", + "function", + "if", + "else", + ].includes(trimmed) + ) { + tokens.push( + + {part} + , + ); + } + // Functions & Special Rivet Terms + else if ( + ["actor", "broadcast", "deathmatch", "isValidMove"].includes(trimmed) + ) { + tokens.push( + + {part} + , + ); + } + // Object Keys / Properties / Methods + else if ( + [ + "state", + "actions", + "players", + "scores", + "map", + "join", + "move", + "connectionId", + "name", + "hp", + "pos", + "x", + "y", + "id", + ].includes(trimmed) + ) { + tokens.push( + + {part} + , + ); + } + // Strings + else if (part.startsWith('"') || part.startsWith("'")) { + tokens.push( + + {part} + , + ); + } + // Numbers + else if (!isNaN(Number(trimmed)) && trimmed !== "") { + tokens.push( + + {part} + , + ); + } + // Default (punctuation, variables like 'c', etc) + else { + tokens.push( + + {part} + , + ); + } + }); + + if (comment) { + tokens.push( + + {comment} + , + ); + } + + return tokens; + })()} + +
+ ))} +
+
+
+
+ ); +}; + +const SolutionCard = ({ title, description, icon: Icon, color = "red" }) => { + const getColorClasses = (col) => { + switch (col) { + case "orange": + return { + bg: "bg-orange-500/10", + text: "text-orange-400", + hoverBg: "group-hover:bg-orange-500/20", + border: "border-orange-500/80", + glow: "rgba(249,115,22,0.1)", + }; + case "blue": + return { + bg: "bg-blue-500/10", + text: "text-blue-400", + hoverBg: "group-hover:bg-blue-500/20", + border: "border-blue-500/80", + glow: "rgba(59,130,246,0.1)", + }; + case "purple": + return { + bg: "bg-purple-500/10", + text: "text-purple-400", + hoverBg: "group-hover:bg-purple-500/20", + border: "border-purple-500/80", + glow: "rgba(168,85,247,0.1)", + }; + case "emerald": + return { + bg: "bg-emerald-500/10", + text: "text-emerald-400", + hoverBg: "group-hover:bg-emerald-500/20", + border: "border-emerald-500/80", + glow: "rgba(16,185,129,0.1)", + }; + case "red": + return { + bg: "bg-red-500/10", + text: "text-red-400", + hoverBg: "group-hover:bg-red-500/20", + border: "border-red-500/80", + glow: "rgba(239,68,68,0.1)", + }; + default: + return { + bg: "bg-red-500/10", + text: "text-red-400", + hoverBg: "group-hover:bg-red-500/20", + border: "border-red-500/80", + glow: "rgba(239,68,68,0.1)", + }; + } + }; + const c = getColorClasses(color); + + return ( +
+ {/* Top Shine Highlight */} +
+ + {/* Soft Glow */} +
+ + {/* Sharp Edge Highlight (Masked & Shortened) */} +
+ +
+
+ +
+

{title}

+
+

+ {description} +

+
+ ); +}; + +// --- Page Sections --- +const Hero = () => ( +
+
+ +
+
+
+ + + + Game Servers.
+ Serverless. +
+ + + Launch an authoritative game server for every match instantly. Scale to millions of + concurrent players without managing fleets or Kubernetes. + + + + + + +
+ +
+
+
+ { + c.state.players[c.connectionId] = { name, hp: 100, pos: {x:0, y:0} }; + c.broadcast("player_join", c.state.players); + }, + + move: (c, { x, y }) => { + // Authoritative movement validation + if (isValidMove(c.state.map, x, y)) { + c.state.players[c.connectionId].pos = { x, y }; + c.broadcast("update", { id: c.connectionId, x, y }); + } + } + } +});`} + /> +
+
+
+
+
+); + +const GameLoopArchitecture = () => { + const [tick, setTick] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + setTick((t) => t + 1); + }, 500); + return () => clearInterval(interval); + }, []); + + return ( +
+
+
+ + The Game Loop + + + Traditional serverless functions die after a request. Rivet Actors stay alive, + maintaining the game state in memory and ticking the simulation as long as players are + connected. + +
+ +
+ {/* Visualization */} +
+ {/* Central Actor (Server) */} +
+
+
+ +
TICK: {tick}
+
+ + {/* Packets */} +
+ {/* Outgoing Update (Broadcast) */} +
+
+
+ + {/* Clients */} +
+ {/* Client 1 */} +
+
+ +
+ Player 1 + {/* Incoming Input */} +
+
+ + {/* Client 2 */} +
+
+ +
+ Player 2 + {/* Incoming Input P2 */} +
+
+
+ + {/* Server Console */} +
+
server.tick({tick - 1})
+
+ server.tick({tick}) > Broadcasting State Snapshot +
+
+ {tick % 2 !== 0 ? ( + + < Player 1 Input: Move(x: 12, y: 40) + + ) : ( + + < Player 2 Input: Attack(target: P1) + + )} +
+
+
+ + {/* Feature List */} +
+ +

+ + Authoritative Logic +

+

+ Run your game logic (movement, hit detection, inventory) on the server to prevent + cheating. The Actor is the single source of truth. +

+
+
+ +

+ + Instant Connectivity +

+

+ Clients connect directly to the specific Actor instance hosting their match via + WebSockets. No database polling latency. +

+
+
+ +

+ + Persistence on Exit +

+

+ When the match ends, the final state (scores, loot, XP) is automatically saved to + disk. +

+
+
+
+
+
+ ); +}; + +const GameFeatures = () => { + const features = [ + { + title: "Lobby Management", + description: + "Create persistent lobby actors that hold players before a match starts. Handle chat, loadouts, and ready states.", + icon: Users, + color: "red", + }, + { + title: "Matchmaking", + description: + "Use a singleton 'Matchmaker' actor to queue players and spawn new Match actors when groups are formed.", + icon: Target, + color: "blue", + }, + { + title: "Turn-Based Games", + description: + "Perfect for card games or board games. Actors can sleep for days between turns without incurring compute costs.", + icon: Clock, + color: "orange", + }, + { + title: "Leaderboards", + description: + "High-throughput counters and sorting in memory. Update scores instantly without hammering a database.", + icon: Trophy, + color: "purple", + }, + { + title: "Economy & Inventory", + description: + "Transactional state for trading items or currency. Ensure no item duplication glitches with serialized execution.", + icon: CreditCard, + color: "emerald", + }, + { + title: "Spectator Mode", + description: + "Allow thousands of users to subscribe to a match actor to watch real-time updates without affecting player latency.", + icon: Eye, + color: "red", + }, + ]; + + return ( +
+
+
+ + Built for Multiplayer + + + Infrastructure primitives that understand the needs of modern games. + +
+ +
+ {features.map((feat, idx) => ( + + + + ))} +
+
+
+ ); +}; + +const UseCases = () => ( +
+
+
+
+ + + Real-Time Strategy (RTS) + + + A persistent world 4X strategy game where thousands of players move armies on a shared + map. + + + {[ + "Sharding: Map divided into hex grids, each controlled by an Actor", + "Fog of War: Calculated on server, only visible units sent to client", + "Persistence: Game state survives server updates seamlessly", + ].map((item, i) => ( +
  • +
    + +
    + {item} +
  • + ))} +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    Sector: Alpha-9
    +
    Units: 4,291 Active
    +
    +
    +
    + Live +
    +
    +
    +
    + Tick Rate + 20Hz +
    +
    +
    +
    +
    + Combat resolved in Grid[44,12]. 12 units lost. Updating clients... +
    +
    +
    + +
    +
    +
    +); + +const Ecosystem = () => ( +
    +
    + + Integrates with your engine + +
    + {["Unity", "Unreal Engine", "Godot", "Phaser", "Three.js", "PlayCanvas"].map( + (tech, i) => ( + + {tech} + + ), + )} +
    +
    +
    +); + +export default function GameServersPage() { + return ( +
    +
    + + + + + + + {/* CTA Section */} +
    +
    + + Launch day ready. + + + Focus on the gameplay. Let Rivet handle the state, scaling, and persistence. + + + + + +
    +
    +
    +
    + ); +} + diff --git a/website/src/app/(v2)/[section]/[[...page]]/layout.tsx b/website/src/app/(v2)/[section]/[[...page]]/layout.tsx index d93586f18d..1a6212d06a 100644 --- a/website/src/app/(v2)/[section]/[[...page]]/layout.tsx +++ b/website/src/app/(v2)/[section]/[[...page]]/layout.tsx @@ -1,69 +1,3 @@ -import { Header } from "@/components/v2/Header"; -import { findActiveTab, findPageForHref, Sitemap } from "@/lib/sitemap"; -import { sitemap } from "@/sitemap/mod"; -import { Button } from "@rivet-gg/components"; -import Link from "next/link"; -import type { CSSProperties } from "react"; -import { buildFullPath, buildPathComponents } from "./util"; -import { NavigationStateProvider } from "@/providers/NavigationStateProvider"; -import { Tree } from "@/components/DocsNavigation"; - -function Subnav({ path }: { path: string[] }) { - const fullPath = buildFullPath(path); - return ( -
    - {sitemap.map((tab, i) => { - const isActive = findPageForHref(fullPath, tab); - return ( - - ); - })} -
    - ); -} - -export default function Layout({ params: { section, page }, children }) { - const path = buildPathComponents(section, page); - const fullPath = buildFullPath(path); - const foundTab = findActiveTab(fullPath, sitemap as Sitemap); - - return ( - -
    } - variant="full-width" - mobileSidebar={ - foundTab?.tab.sidebar ? ( - - ) : null - } - /> -
    -
    - {children} -
    -
    - - ); +export default function Layout({ children }) { + return <>{children}; } diff --git a/website/src/app/(v2)/[section]/doc-page.tsx b/website/src/app/(v2)/[section]/doc-page.tsx new file mode 100644 index 0000000000..334662ec70 --- /dev/null +++ b/website/src/app/(v2)/[section]/doc-page.tsx @@ -0,0 +1,206 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { Button } from "@rivet-gg/components"; +import { faPencil, Icon } from "@rivet-gg/icons"; +import clsx from "clsx"; +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { Comments } from "@/components/Comments"; +import { DocsNavigation } from "@/components/DocsNavigation"; +import { DocsPageDropdown } from "@/components/DocsPageDropdown"; +import { DocsTableOfContents } from "@/components/DocsTableOfContents"; +import { Prose } from "@/components/Prose"; +import { findActiveTab, type Sitemap } from "@/lib/sitemap"; +import { sitemap } from "@/sitemap/mod"; +import { buildFullPath, buildPathComponents, VALID_SECTIONS } from "./util"; + +interface Param { + section: string; + page?: string[]; +} + +function createParamsForFile(section: string, file: string): Param { + const step1 = file.replace("index.mdx", ""); + const step2 = step1.replace(".mdx", ""); + const step3 = step2.split("/"); + const step4 = step3.filter((x) => x.length > 0); + + return { + section, + page: step4.length > 0 ? step4 : undefined, + }; +} + +async function loadContent(pathComponents: string[]) { + const module = pathComponents.join("/"); + try { + return { + path: `${module}.mdx`, + component: await import(`@/content/${module}.mdx`), + }; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "MODULE_NOT_FOUND") { + try { + const indexModule = `${module}/index`; + return { + path: `${indexModule}.mdx`, + component: await import(`@/content/${indexModule}.mdx`), + }; + } catch (indexError) { + if ((indexError as NodeJS.ErrnoException).code === "MODULE_NOT_FOUND") { + return notFound(); + } + throw indexError; + } + } + throw error; + } +} + +export async function generateDocMetadata( + section: string, + page?: string[], +): Promise { + const pathComponents = buildPathComponents(section, page); + const { + component: { title, description }, + } = await loadContent(pathComponents); + + const fullPath = buildFullPath(pathComponents); + const canonicalUrl = `https://www.rivet.dev${fullPath}/`; + + return { + title: `${title} - Rivet`, + description, + alternates: { + canonical: canonicalUrl, + }, + }; +} + +export async function renderDocPage(section: string, page?: string[]) { + if (!VALID_SECTIONS.includes(section)) { + return notFound(); + } + + const pathComponents = buildPathComponents(section, page); + const { + path: componentSourcePath, + component: { default: Content, tableOfContents, title, description }, + } = await loadContent(pathComponents); + + const fullPath = buildFullPath(pathComponents); + const foundTab = findActiveTab(fullPath, sitemap as Sitemap); + const parentPage = foundTab?.page.parent; + + const markdownPath = componentSourcePath + .replace(/\.mdx$/, "") + .replace(/\/index$/, "") + .replace(/\\/g, "/"); + + return ( + <> + +
    +
    +
    +
    +
    + +
    +
    + + {parentPage && ( +
    + {parentPage.title} +
    + )} + +
    +
    + {tableOfContents && ( + + )} +
    +
    + + ); +} + +export async function getAllDocParams(): Promise> { + const staticParams: Array = []; + const seenParams = new Set(); + + for (const section of VALID_SECTIONS) { + const dir = path.join(process.cwd(), "src", "content", section); + + try { + const baseKey = `${section}`; + if (!seenParams.has(baseKey)) { + seenParams.add(baseKey); + staticParams.push({ section }); + } + + const dirs = await fs.readdir(dir, { recursive: true }); + const files = dirs.filter((file) => file.endsWith(".mdx")); + + for (const file of files) { + const param = createParamsForFile(section, file); + const finalParam: Param = + param.page === undefined + ? { section: param.section } + : { section: param.section, page: param.page }; + + const key = finalParam.page + ? `${finalParam.section}/${finalParam.page.join("/")}` + : finalParam.section; + + if (!seenParams.has(key)) { + seenParams.add(key); + staticParams.push(finalParam); + } + } + } catch (error) { + const baseKey = `${section}`; + if (!seenParams.has(baseKey)) { + seenParams.add(baseKey); + staticParams.push({ section }); + } + } + } + + return staticParams; +} + diff --git a/website/src/app/(v2)/[section]/layout.tsx b/website/src/app/(v2)/[section]/layout.tsx new file mode 100644 index 0000000000..acf6fbdb27 --- /dev/null +++ b/website/src/app/(v2)/[section]/layout.tsx @@ -0,0 +1,76 @@ +import { Header } from "@/components/v2/Header"; +import { findActiveTab, findPageForHref, Sitemap } from "@/lib/sitemap"; +import { sitemap } from "@/sitemap/mod"; +import { Button } from "@rivet-gg/components"; +import Link from "next/link"; +import type { CSSProperties, ReactNode } from "react"; +import { buildFullPath, buildPathComponents } from "./util"; +import { NavigationStateProvider } from "@/providers/NavigationStateProvider"; +import { Tree } from "@/components/DocsNavigation"; + +function Subnav({ path }: { path: string[] }) { + const fullPath = buildFullPath(path); + return ( +
    + {sitemap.map((tab, i) => { + const isActive = findPageForHref(fullPath, tab); + return ( + + ); + })} +
    + ); +} + +export default function Layout({ + params: { section, page }, + children, +}: { + params: { section: string; page?: string[] }; + children: ReactNode; +}) { + const path = buildPathComponents(section, page); + const fullPath = buildFullPath(path); + const foundTab = findActiveTab(fullPath, sitemap as Sitemap); + + return ( + +
    } + variant="full-width" + mobileSidebar={ + foundTab?.tab.sidebar ? ( + + ) : null + } + /> +
    +
    + {children} +
    +
    + + ); +} + diff --git a/website/src/app/(v2)/[section]/util.ts b/website/src/app/(v2)/[section]/util.ts new file mode 100644 index 0000000000..9015299740 --- /dev/null +++ b/website/src/app/(v2)/[section]/util.ts @@ -0,0 +1,19 @@ +export const VALID_SECTIONS = ["docs", "guides"]; + +export function buildPathComponents( + section: string, + page?: string[], +): string[] { + let defaultedPage = page ?? []; + + if (defaultedPage[defaultedPage.length - 1] === "index") { + defaultedPage = defaultedPage.slice(0, -1); + } + + return [section, ...defaultedPage]; +} + +export function buildFullPath(pathComponents: string[]): string { + return `/${pathComponents.join("/")}`; +} + diff --git a/website/src/app/layout.tsx b/website/src/app/layout.tsx index 8dc64bc5aa..6f528e183d 100644 --- a/website/src/app/layout.tsx +++ b/website/src/app/layout.tsx @@ -13,7 +13,7 @@ else if (process.env.CF_PAGES_URL) export const metadata: Metadata = { metadataBase, - title: "Rivet - Build and scale stateful workloads", + title: "Rivet - Stateful Backends. Finally Solved.", description: "Rivet is a library for long-lived processes with durable state, realtime, and scalability. Easily self-hostable and works with your infrastructure.", twitter: { diff --git a/website/src/components/v2/Header.tsx b/website/src/components/v2/Header.tsx index 8718ce3a14..cd93bb08c2 100644 --- a/website/src/components/v2/Header.tsx +++ b/website/src/components/v2/Header.tsx @@ -306,7 +306,7 @@ export function Header({
    } subnav={subnav} - support={null} + support={<>} links={
    {!learnMode && ( @@ -379,10 +379,11 @@ function DocsMobileNavigation({ tree }) { ]; const mainLinks = [ - { href: "/solutions", label: "Solutions", isDropdown: true }, + { href: "/", label: "Home" }, { href: "/docs", label: "Documentation" }, { href: "/changelog", label: "Changelog" }, { href: "/pricing", label: "Pricing" }, + { href: "https://dashboard.rivet.dev/", label: "Dashboard" }, ]; const solutions = [