From 5965a6ee3eb64cb7a7a58e12a7204a72efe903f0 Mon Sep 17 00:00:00 2001 From: Nicholas Kissel Date: Tue, 18 Nov 2025 21:26:47 -0800 Subject: [PATCH 01/13] chore: v5 website changes --- .../src/app/(v2)/(marketing)/(index)/page.tsx | 56 +- .../(index)/sections/CodeWalkthrough.tsx | 221 ++++++++ .../(index)/sections/ConceptSection.tsx | 217 ++++++++ .../(index)/sections/FeaturesSection.tsx | 480 +++++++++++++----- .../(index)/sections/HostingSection.tsx | 121 +++++ .../(index)/sections/IntegrationsSection.tsx | 135 +++++ .../(index)/sections/ObservabilitySection.tsx | 124 +++++ .../(index)/sections/RedesignedCTA.tsx | 51 ++ .../(index)/sections/RedesignedHero.tsx | 195 +++++++ .../(index)/sections/SolutionSection.tsx | 4 +- .../(index)/sections/SolutionsSection.tsx | 115 +++++ .../(index)/sections/StatsSection.tsx | 31 ++ website/src/components/FadeIn.tsx | 92 ++++ 13 files changed, 1678 insertions(+), 164 deletions(-) create mode 100644 website/src/app/(v2)/(marketing)/(index)/sections/CodeWalkthrough.tsx create mode 100644 website/src/app/(v2)/(marketing)/(index)/sections/ConceptSection.tsx create mode 100644 website/src/app/(v2)/(marketing)/(index)/sections/HostingSection.tsx create mode 100644 website/src/app/(v2)/(marketing)/(index)/sections/IntegrationsSection.tsx create mode 100644 website/src/app/(v2)/(marketing)/(index)/sections/ObservabilitySection.tsx create mode 100644 website/src/app/(v2)/(marketing)/(index)/sections/RedesignedCTA.tsx create mode 100644 website/src/app/(v2)/(marketing)/(index)/sections/RedesignedHero.tsx create mode 100644 website/src/app/(v2)/(marketing)/(index)/sections/SolutionsSection.tsx create mode 100644 website/src/app/(v2)/(marketing)/(index)/sections/StatsSection.tsx create mode 100644 website/src/components/FadeIn.tsx diff --git a/website/src/app/(v2)/(marketing)/(index)/page.tsx b/website/src/app/(v2)/(marketing)/(index)/page.tsx index 9e41d1103d..67cb84c996 100644 --- a/website/src/app/(v2)/(marketing)/(index)/page.tsx +++ b/website/src/app/(v2)/(marketing)/(index)/page.tsx @@ -1,42 +1,34 @@ "use client"; -// Import new sections -import { NewHeroSection } from "./sections/NewHeroSection"; -import { SocialProofSection } from "./sections/SocialProofSection"; -import { ProblemSection } from "./sections/ProblemSection"; -import { SolutionSection } from "./sections/SolutionSection"; -import { NewFeaturesBento } from "./sections/NewFeaturesBento"; -import { NewUseCases } from "./sections/NewUseCases"; -import { NewCTASection } from "./sections/NewCTASection"; +// Import redesigned sections +import { RedesignedHero } from "./sections/RedesignedHero"; +import { StatsSection } from "./sections/StatsSection"; +import { ConceptSection } from "./sections/ConceptSection"; +import { CodeWalkthrough } from "./sections/CodeWalkthrough"; +import { ObservabilitySection } from "./sections/ObservabilitySection"; +import { FeaturesSection } from "./sections/FeaturesSection"; +import { SolutionsSection } from "./sections/SolutionsSection"; +import { HostingSection } from "./sections/HostingSection"; +import { IntegrationsSection } from "./sections/IntegrationsSection"; +import { RedesignedCTA } from "./sections/RedesignedCTA"; import { ScrollObserver } from "@/components/ScrollObserver"; export default function IndexPage() { return ( -
- {/* Main container */} -
- {/* Hero Section */} - - - {/* Social Proof */} - - - {/* The Problem */} - - - {/* The Solution */} - - - {/* Features Bento */} - - - {/* Use Cases */} - - - {/* Final CTA */} - -
+
+
+ + + + + + + + + + +
); diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/CodeWalkthrough.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/CodeWalkthrough.tsx new file mode 100644 index 0000000000..22bbefdda8 --- /dev/null +++ b/website/src/app/(v2)/(marketing)/(index)/sections/CodeWalkthrough.tsx @@ -0,0 +1,221 @@ +"use client"; + +import { useEffect, useState, useRef } from "react"; +import { motion } from "framer-motion"; + +export const CodeWalkthrough = () => { + const [activeStep, setActiveStep] = useState(0); + const observerRefs = useRef([]); + + const steps = [ + { + title: "Define the Actor", + description: + "Start by importing and exporting an actor definition. This creates a specialized serverless function that maintains its own isolated memory space.", + lines: [0, 2, 14], + }, + { + title: "Declare Persistent State", + description: + "Define the shape of your data. This state object is automatically persisted to disk and loaded into memory when the actor wakes up. No database queries needed.", + lines: [3], + }, + { + title: "Write RPC Actions", + description: + "Actions are just functions. They run directly in the actor's memory space with zero network latency to access the state.", + lines: [5, 6, 12, 13], + }, + { + title: "Mutate State Directly", + description: + "Just modify the state variable. Rivet detects the changes and handles the persistence and replication for you.", + lines: [7, 8], + }, + { + title: "Broadcast Realtime Events", + description: + "Push updates to all connected clients instantly using WebSockets. It's built right into the context object.", + lines: [10], + }, + ]; + + const codeLines = [ + `import { actor } from "rivetkit";`, + ``, + `export const chatRoom = actor({`, + ` state: { messages: [] },`, + ``, + ` actions: {`, + ` postMessage: (c, text) => {`, + ` const msg = { text, at: Date.now() };`, + ` c.state.messages.push(msg);`, + ` `, + ` c.broadcast("newMessage", msg);`, + ` return "sent";`, + ` }`, + ` }`, + `});`, + ]; + + useEffect(() => { + const options = { + root: null, + rootMargin: "-40% 0px -40% 0px", + threshold: 0, + }; + + const observer = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const index = Number(entry.target.dataset.index); + setActiveStep(index); + } + }); + }, options); + + observerRefs.current.forEach((ref) => { + if (ref) observer.observe(ref); + }); + + return () => observer.disconnect(); + }, []); + + return ( +
+
+ +

How it works

+

+ Rivet makes backend development feel like frontend development. Define your state, write your + logic, and let the engine handle the rest. +

+
+ +
+ {/* Sticky Code Block */} +
+
+
+
+
+
+
+
+
+ chat_room.ts +
+
+ {codeLines.map((line, idx) => { + const isHighlighted = steps[activeStep].lines.includes(idx); + const isDimmed = !isHighlighted; + + return ( +
+ + {idx + 1} + + + {line.split(/(\s+)/).map((part, i) => { + if ( + part.trim() === "import" || + part.trim() === "export" || + part.trim() === "const" || + part.trim() === "return" + ) + return ( + + {part} + + ); + if (part.trim() === "actor" || part.trim() === "broadcast") + return ( + + {part} + + ); + if (part.includes('"')) + return ( + + {part} + + ); + if (part.includes("//")) + return ( + + {part} + + ); + return part; + })} + +
+ ); + })} +
+
+
+
+ + {/* Scrolling Steps */} +
+ {steps.map((step, idx) => ( +
(observerRefs.current[idx] = el)} + className={`transition-all duration-500 p-6 rounded-2xl border backdrop-blur-sm ${ + idx === activeStep + ? "bg-white/[0.04] border-white/20 shadow-[0_0_30px_-10px_rgba(255,255,255,0.1)]" + : "bg-white/[0.02] border-white/5 opacity-50 hover:opacity-100 hover:bg-white/[0.05] hover:border-white/10" + }`} + > +
+
+ {idx + 1} +
+

+ {step.title} +

+
+

{step.description}

+ + {/* Mobile Only Code Snippet */} +
+ {step.lines.map((lineIdx) => ( +
+ {codeLines[lineIdx]} +
+ ))} +
+
+ ))} +
+
+
+
+
+ ); +}; + diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/ConceptSection.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/ConceptSection.tsx new file mode 100644 index 0000000000..d85fe4f6c5 --- /dev/null +++ b/website/src/app/(v2)/(marketing)/(index)/sections/ConceptSection.tsx @@ -0,0 +1,217 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Server, Box, Database, Cpu, Check } from "lucide-react"; +import { motion } from "framer-motion"; + +const ArchitectureComparison = () => { + const [activeStep, setActiveStep] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + setActiveStep((prev) => (prev + 1) % 4); + }, 2000); + return () => clearInterval(interval); + }, []); + + return ( +
+ {/* Traditional Serverless */} + +
+
+ +
+

Traditional Serverless

+
+ +
+
+
+ Client +
+
+
+
+
+ Lambda +
+
+ +
+
+
+
+
+ External DB +
+
+
+

+ State must be fetched from a remote DB for every request. +
+ High Latency • Connection Limits +

+ + + {/* Rivet Actor */} + +
+
+
+ +
+

Rivet Actor Model

+
+ +
+
+
+ Client +
+
+
+
+
+ + {/* The Actor */} +
+
+
Actor
+
+
+ + Compute +
+
+ + In-Mem State +
+
+ {/* Pulse effect */} + {activeStep % 2 !== 0 && ( +
+ )} +
+
+
+

+ State lives with the compute in memory. +
+ Zero Latency • Realtime • Persistent +

+ +
+ ); +}; + +export const ConceptSection = () => ( +
+
+
+ +

+ Think in Actors, +
+ not just Functions. +

+
+

+ What is an Actor? +
+ An Actor is a tiny, isolated server that holds its own data in memory. Unlike a stateless + function that forgets everything after it runs, an Actor remembers. +

+

+ Why use them? +
+ When you need to manage state for a specific entity—like a Chat Room, a Game Match, or a User + Session—fetching that data from a database every 100ms is slow and expensive. Actors keep + that data in RAM, right next to your logic. +

+
+
+
+ +
+
+
+
+); + diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/FeaturesSection.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/FeaturesSection.tsx index d1498f3853..0bfb6fe329 100644 --- a/website/src/app/(v2)/(marketing)/(index)/sections/FeaturesSection.tsx +++ b/website/src/app/(v2)/(marketing)/(index)/sections/FeaturesSection.tsx @@ -1,143 +1,361 @@ "use client"; -import { useRef } from "react"; -import { IconWithSpotlight } from "./IconWithSpotlight"; - -interface FeatureCardProps { - title: string; - description: string; - className?: string; - docLink?: string; - iconPath?: string; -} - -function FeatureCard({ - title, - description, - className = "", - docLink, - iconPath, -}: FeatureCardProps) { - const cardRef = useRef(null); - - const handleMouseMove = (e: React.MouseEvent) => { - if (!cardRef.current) return; - const card = cardRef.current; - - // Find the icon container and convert coordinates to icon-relative - const iconContainer = card.querySelector('.icon-spotlight-container') as HTMLElement; - if (!iconContainer) return; - - // Get the icon's position relative to viewport - const iconRect = iconContainer.getBoundingClientRect(); - - // Calculate mouse position relative to the icon (not the card) - const x = ((e.clientX - iconRect.left) / iconRect.width) * 100; - const y = ((e.clientY - iconRect.top) / iconRect.height) * 100; - - // Set CSS custom properties on the icon container - iconContainer.style.setProperty('--mouse-x', `${x}%`); - iconContainer.style.setProperty('--mouse-y', `${y}%`); - }; +import { Zap, Database, Cpu, AlertCircle } from "lucide-react"; +import { motion } from "framer-motion"; - return ( - -
- {iconPath && ( - <> -
-
- +const FeatureCard = ({ title, description, code, graphic }) => ( +
+ {/* Graphic Area */} +
+ {graphic} +
+ + {/* Content */} +
+

{title}

+

{description}

+ + {code && ( +
+ {code} +
+ )} +
+
+); + +export const FeaturesSection = () => { + const features = [ + { + title: "Long-Lived Compute", + description: + "Like Lambda, but with memory. No 5-minute timeouts, no state loss. Your logic runs as long as your product does.", + code: "persistent.process()", + graphic: ( +
+ {/* Stateless Side */} +
+
+ stateless
- - )} -

{title}

-

- {description} -

-
-
- ); -} +
+
+
+
+ + {/* Stateful Side */} +
+
+ stateful +
+
+
+
+
+
+
+ ), + }, + { + title: "In-Memory Speed", + description: + "State lives beside your compute. Reads and writes are in-memory—no cache invalidation, no round-trips.", + code: "read(<1ms)", + graphic: ( +
+
+ {/* The Actor Box */} +
+
+ Actor +
+
+ {/* Internal Pipe */} +
+
+
+ + {/* CPU Node */} +
+
+ +
+ Compute +
+ + {/* Local State Node */} +
+
+ +
+ In-Mem +
+
+
+ + {/* External Pipe */} +
+
+
+ + {/* External DB */} +
+
+ +
+ DB +
+
+
+ ), + }, + { + title: "Realtime, Built-in", + description: + "WebSockets and SSE out of the box. Broadcast updates with one line—no extra infrastructure, no pub/sub layer.", + code: "c.broadcast()", + graphic: ( +
+ {/* Center Node */} +
+ + {/* Ripples */} +
+
+
+ + {/* Satellite Nodes */} +
+
+
+
+ ), + }, + { + title: "Automatic Hibernation", + description: + "Actors automatically hibernate to save costs and wake instantly on demand. You only pay for work done.", + code: "idle → sleep()", + graphic: ( +
+ {/* Packet moves in from left */} +
+ + {/* Actor Container */} +
+
+ {/* Awake State Content (Zap) */} +
+ +
+ + {/* Sleep State Content (Zzz) */} +
+
+ + Z + + + z + + + z + +
+
+
+
+
+ ), + }, + { + title: "Open Source", + description: + "No lock-in. Run on your platform of choice or bare metal with the same API and mental model.", + code: "apache-2.0", + graphic: ( +
+
+
+
+
+
+
+ $ cargo run +
+
Compiling rivet...
+
Finished dev profile
+
+ > + +
+
+ ), + }, + { + title: "Resilient by Design", + description: + "Automatic failover and restarts maintain state integrity. Your actors survive crashes, deploys, noisy neighbors.", + code: "uptime: 99.9%", + graphic: ( +
+ {/* Pulse Ring Background */} +
+ + {/* The Process Shell */} +
+ {/* The Persistent State (Core) */} +
+ +
+ + {/* Crash indicator overlay */} +
+ +
+
+ + {/* "Rebooting" Spinner ring */} +
+
+ ), + }, + ]; -export function FeaturesSection() { return ( -
-
-
-

- Built for Modern Applications -

-

- Everything you need to build fast, scalable, and realtime - applications without the complexity. -

+
+
+
+ + Everything you need for +
+ stateful workloads. +
+ + Rivet handles the hard parts of distributed systems: sharding, coordination, and persistence. + You just write the logic. +
-
- - - - - - - - - - - - - - - - - +
+ {features.map((feature, idx) => ( + + + + ))}
+ + {/* Custom Animations for Graphics */} +
); -} +}; diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/HostingSection.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/HostingSection.tsx new file mode 100644 index 0000000000..c71d9f0dbc --- /dev/null +++ b/website/src/app/(v2)/(marketing)/(index)/sections/HostingSection.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { Github, Cloud, Server, Check } from "lucide-react"; +import { motion } from "framer-motion"; + +export const HostingSection = () => ( +
+
+
+ + Deploy your way. + + + Start with the open-source binary on your laptop. Scale with Rivet Cloud. Go hybrid when you need + total control over data residency. + +
+ +
+ {/* Card 1: Open Source */} + +
+ +
+

Open Source

+

+ The core engine is 100% open source and compiles to a single binary. Run it locally, on a VPS, + or inside Kubernetes. No vendor lock-in. +

+
+
+ $ + curl -fsSL https://rivet.gg/install.sh | sh +
+
+ $ + rivet-server start +
+
+
+ + {/* Card 2: Rivet Cloud */} + +
+ +
+

Rivet Cloud

+

+ The fully managed control plane. We handle the orchestration, monitoring, and edge routing. You + just connect your compute and go. +

+
    + {["Managed Orchestration", "Global Edge Network", "Instant Scaling"].map((item) => ( +
  • + {item} +
  • + ))} +
+
+ + {/* Card 3: Hybrid */} + +
+ +
+

Hybrid & On-Prem

+

+ Keep sensitive actors on your own private VPC for compliance, while offloading public traffic + to Rivet Cloud. Managed via a single dashboard. +

+
+
+
+ +
+ Private +
+
+
+
+ +
+ Cloud +
+
+ +
+
+
+); + diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/IntegrationsSection.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/IntegrationsSection.tsx new file mode 100644 index 0000000000..69a86ae799 --- /dev/null +++ b/website/src/app/(v2)/(marketing)/(index)/sections/IntegrationsSection.tsx @@ -0,0 +1,135 @@ +"use client"; + +import { Box, LayoutGrid, Terminal, Wrench } from "lucide-react"; +import { motion } from "framer-motion"; + +export const IntegrationsSection = () => ( +
+
+
+ +

+ Stack Agnostic. +

+

+ Rivet actors are just standard TypeScript. They run in Docker, connect via WebSockets, and + integrate effortlessly with your existing stack. +

+
+
+ +
+ {/* Category 1: Infrastructure */} + +
+
+ +
+

Infrastructure

+
+
+ {["Docker", "Kubernetes", "Fly.io", "Railway", "AWS ECS"].map((tech) => ( + + {tech} + + ))} +
+
+ + {/* Category 2: Frameworks */} + +
+
+ +
+

Frameworks

+
+
+ {["Next.js", "Remix", "React", "Vue", "Svelte", "Unity", "Godot"].map((tech) => ( + + {tech} + + ))} +
+
+ + {/* Category 3: Runtimes */} + +
+
+ +
+

Runtimes

+
+
+ {["Node.js", "Bun", "Deno", "Cloudflare Workers"].map((tech) => ( + + {tech} + + ))} +
+
+ + {/* Category 4: Tools */} + +
+
+ +
+

Tools

+
+
+ {["TypeScript", "ESLint", "Prettier", "Vite", "Turborepo"].map((tech) => ( + + {tech} + + ))} +
+
+
+
+
+); + diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/ObservabilitySection.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/ObservabilitySection.tsx new file mode 100644 index 0000000000..981174b332 --- /dev/null +++ b/website/src/app/(v2)/(marketing)/(index)/sections/ObservabilitySection.tsx @@ -0,0 +1,124 @@ +"use client"; + +import { Eye, Activity, Terminal, Wifi, ArrowRight, Play } from "lucide-react"; +import { motion } from "framer-motion"; + +export const ObservabilitySection = () => { + const features = [ + { + title: "Live State Inspection", + description: + "View and edit your actor state in real-time as messages are sent and processed", + icon: , + }, + { + title: "Event Monitoring", + description: + "See all events happening in your actor in real-time - track every state change and action as it happens", + icon: , + }, + { + title: "REPL", + description: + "Debug your actor in real-time - call actions, subscribe to events, and interact directly with your code", + icon: , + }, + { + title: "Connection Inspection", + description: "Monitor active connections with state and parameters for each client", + icon: , + }, + ]; + + return ( +
+
+
+ + Built-In Observability + + + Powerful debugging and monitoring tools that work seamlessly from local development to production + at scale. + + + + + +
+ +
+ {/* Feature List */} +
+ {features.map((feat, idx) => ( + +
+ {feat.icon} +
+
+

{feat.title}

+

{feat.description}

+
+
+ ))} +
+ + {/* Empty Window Frame */} + +
+
+ {/* Window Bar */} +
+
+
+
+
+ {/* Content Area - Placeholder for Image */} +
+
Dashboard Placeholder
+
+
+ +
+
+
+ ); +}; + diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/RedesignedCTA.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/RedesignedCTA.tsx new file mode 100644 index 0000000000..82e0ce12fd --- /dev/null +++ b/website/src/app/(v2)/(marketing)/(index)/sections/RedesignedCTA.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { motion } from "framer-motion"; + +export const RedesignedCTA = () => ( +
+
+ +
+ + Ready to build better backends? + + + Join thousands of developers building the next generation of{" "} +
+ realtime, stateful applications with Rivet. +
+ + + + +
+
+); + diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/RedesignedHero.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/RedesignedHero.tsx new file mode 100644 index 0000000000..0d0f1740ce --- /dev/null +++ b/website/src/app/(v2)/(marketing)/(index)/sections/RedesignedHero.tsx @@ -0,0 +1,195 @@ +"use client"; + +import { Terminal, ArrowRight, Check, Copy } from "lucide-react"; +import { useState } from "react"; +import { motion } from "framer-motion"; + +const CodeBlock = ({ code, fileName = "actor.ts" }) => { + const [copied, setCopied] = useState(false); + + const handleCopy = () => { + navigator.clipboard.writeText(code).catch(() => {}); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + 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 + // Note: this is still basic but better than before + 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"].includes(trimmed)) { + tokens.push({part}); + } + // Functions & Special Rivet Terms + else if (["actor", "broadcast"].includes(trimmed)) { + tokens.push({part}); + } + // Object Keys / Properties / Methods + else if (["state", "actions", "increment", "count", "push", "now", "Date"].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; + })()} + +
+ ))} +
+
+
+
+ ); +}; + +export const RedesignedHero = () => ( +
+
+ +
+
+
+ + + Rivet 2.0 is now available + + + + + Stateful Backends.
+ + Finally Solved. + +
+ + + Stop faking state with databases and message queues. Rivet turns your TypeScript code into + durable, distributed actors. No complex infrastructure, no database queries—just state that + persists. + + + + + + +
+ +
+ +
+ { + c.state.count++; + + // Realtime by default + c.broadcast("updated", c.state); + + return c.state.count; + } + } +});`} + /> + +
+
+
+
+); diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/SolutionSection.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/SolutionSection.tsx index 184933accb..f391732492 100644 --- a/website/src/app/(v2)/(marketing)/(index)/sections/SolutionSection.tsx +++ b/website/src/app/(v2)/(marketing)/(index)/sections/SolutionSection.tsx @@ -208,7 +208,9 @@ export function SolutionSection() { {/* Left Side: Sticky Code */}
-
+
+ {/* Top Shine Highlight */} +
ai-agent.ts actor.ts diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/SolutionsSection.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/SolutionsSection.tsx new file mode 100644 index 0000000000..1544fa21eb --- /dev/null +++ b/website/src/app/(v2)/(marketing)/(index)/sections/SolutionsSection.tsx @@ -0,0 +1,115 @@ +"use client"; + +import { + Bot, + FileText, + Workflow, + RefreshCw, + MessageSquare, + Database, + Gamepad2, + Clock, + Gauge, + ArrowRight, +} from "lucide-react"; +import { motion } from "framer-motion"; + +export const SolutionsSection = () => { + const solutions = [ + { + title: "AI Agent", + description: "Build durable AI assistants with persistent memory and realtime streaming", + icon: , + }, + { + title: "Collaborative State", + description: "Collaborative documents with CRDTs and realtime synchronization", + icon: , + }, + { + title: "Workflows", + description: "Durable multi-step workflows with automatic state management", + icon: , + }, + { + title: "Local-First Sync", + description: "Offline-first applications with server synchronization", + icon: , + }, + { + title: "Bots", + description: "Discord, Slack, or autonomous bots with persistent state", + icon: , + }, + { + title: "User Session Store", + description: "Isolated data stores for each user with zero-latency access", + icon: , + }, + { + title: "Multiplayer Game", + description: "Authoritative game servers with realtime state synchronization", + icon: , + }, + { + title: "Background Jobs", + description: "Scheduled and recurring jobs without external queue infrastructure", + icon: , + }, + { + title: "Rate Limiting", + description: "Distributed rate limiting with in-memory counters", + icon: , + }, + ]; + + return ( +
+
+
+ + Build anything stateful. + + + If it needs to remember something, it belongs in an Actor. + +
+ +
+ {solutions.map((solution, idx) => ( + +
+
+
{solution.icon}
+

{solution.title}

+
+ +
+

{solution.description}

+
+ ))} +
+
+
+ ); +}; + diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/StatsSection.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/StatsSection.tsx new file mode 100644 index 0000000000..2476d4eee4 --- /dev/null +++ b/website/src/app/(v2)/(marketing)/(index)/sections/StatsSection.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { motion } from "framer-motion"; + +const StatItem = ({ value, label }) => ( +
+ + {value} + + {label} +
+); + +export const StatsSection = () => ( +
+
+ + + + + + +
+
+); diff --git a/website/src/components/FadeIn.tsx b/website/src/components/FadeIn.tsx new file mode 100644 index 0000000000..84f3953027 --- /dev/null +++ b/website/src/components/FadeIn.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { motion } from "framer-motion"; + +interface FadeInProps { + children: React.ReactNode; + delay?: number; + className?: string; + direction?: "up" | "down" | "left" | "right" | "none"; + duration?: number; + fullWidth?: boolean; +} + +export function FadeIn({ + children, + delay = 0, + className = "", + direction = "up", + duration = 0.5, + fullWidth = false, +}: FadeInProps) { + const variants = { + hidden: { + opacity: 0, + y: direction === "up" ? 20 : direction === "down" ? -20 : 0, + x: direction === "left" ? 20 : direction === "right" ? -20 : 0, + }, + visible: { + opacity: 1, + y: 0, + x: 0, + transition: { + duration: duration, + delay: delay, + ease: [0.25, 0.1, 0.25, 1.0], // Cubic bezier for sleek feel + }, + }, + }; + + return ( + + {children} + + ); +} + +export function FadeInStagger({ + children, + delay = 0, + className = "", + faster = false, +}: { children: React.ReactNode; delay?: number; className?: string; faster?: boolean }) { + return ( + + {children} + + ); +} + +export function FadeInItem({ children, className = "" }: { children: React.ReactNode; className?: string }) { + const variants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.5, + ease: [0.25, 0.1, 0.25, 1.0], + }, + }, + }; + + return ( + + {children} + + ); +} + From f88207d1edcb11a80093387ff9a015b256a7da76 Mon Sep 17 00:00:00 2001 From: Nicholas Kissel Date: Wed, 19 Nov 2025 00:05:07 -0800 Subject: [PATCH 02/13] Add solution pages for agents, games, workflows, user-session-store, and collaborative-state --- .../src/app/(v2)/(marketing)/agent/page.tsx | 740 +++++++++++++++++ .../(marketing)/solutions/agents/page.tsx | 747 ++++++++++++++++++ .../solutions/collaborative-state/page.tsx | 675 ++++++++++++++++ .../(v2)/(marketing)/solutions/games/page.tsx | 704 +++++++++++++++++ .../solutions/user-session-store/page.tsx | 717 +++++++++++++++++ .../(marketing)/solutions/workflows/page.tsx | 705 +++++++++++++++++ 6 files changed, 4288 insertions(+) create mode 100644 website/src/app/(v2)/(marketing)/agent/page.tsx create mode 100644 website/src/app/(v2)/(marketing)/solutions/agents/page.tsx create mode 100644 website/src/app/(v2)/(marketing)/solutions/collaborative-state/page.tsx create mode 100644 website/src/app/(v2)/(marketing)/solutions/games/page.tsx create mode 100644 website/src/app/(v2)/(marketing)/solutions/user-session-store/page.tsx create mode 100644 website/src/app/(v2)/(marketing)/solutions/workflows/page.tsx diff --git a/website/src/app/(v2)/(marketing)/agent/page.tsx b/website/src/app/(v2)/(marketing)/agent/page.tsx new file mode 100644 index 0000000000..da3c57a803 --- /dev/null +++ b/website/src/app/(v2)/(marketing)/agent/page.tsx @@ -0,0 +1,740 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { + Terminal, + Zap, + Globe, + Github, + ArrowRight, + Box, + Database, + Layers, + Check, + Copy, + Cpu, + Server, + RefreshCw, + Clock, + Shield, + Cloud, + Download, + LayoutGrid, + Activity, + Wifi, + Moon, + AlertCircle, + Gamepad2, + MessageSquare, + Bot, + Users, + FileText, + Workflow, + Gauge, + Eye, + Play, + Brain, + Sparkles, + Network, +} from "lucide-react"; +import { motion } from "framer-motion"; +import { ScrollObserver } from "@/components/ScrollObserver"; + +// --- Shared Design Components --- + +const Badge = ({ text, color = "orange" }) => { + const colorClasses = { + orange: "text-[#FF4500] border-[#FF4500]/20 bg-[#FF4500]/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", + }; + + return ( +
+ + {text} +
+ ); +}; + +const CodeBlock = ({ code, fileName = "agent.ts" }) => { + const [copied, setCopied] = useState(false); + + const handleCopy = () => { + const textArea = document.createElement("textarea"); + textArea.value = code; + document.body.appendChild(textArea); + textArea.select(); + try { + document.execCommand("copy"); + } catch (err) { + if (navigator.clipboard) { + navigator.clipboard.writeText(code).catch((e) => console.error(e)); + } + } + document.body.removeChild(textArea); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
+
+
+
+
+
+
+
{fileName}
+ +
+
+
+					{code}
+				
+
+
+ ); +}; + +// --- Refined Agent Card with Soft Glow & Masked Corners --- +const SolutionCard = ({ title, description, icon: Icon, color = "orange" }) => { + const colorClasses = { + orange: { + iconBg: "bg-[#FF4500]/10 text-[#FF4500] group-hover:bg-[#FF4500]/20", + }, + blue: { + iconBg: "bg-blue-500/10 text-blue-500 group-hover:bg-blue-500/20", + }, + purple: { + iconBg: "bg-purple-500/10 text-purple-500 group-hover:bg-purple-500/20", + }, + emerald: { + iconBg: "bg-emerald-500/10 text-emerald-500 group-hover:bg-emerald-500/20", + }, + }; + + const colors = colorClasses[color] || colorClasses.orange; + + return ( +
+ {/* Top Shine Highlight */} +
+ + {/* Soft Glow (Gradient) */} +
+ +
+
+ +
+

{title}

+
+

+ {description} +

+
+ ); +}; + +// --- Page Sections --- + +const Hero = () => ( +
+
+ +
+
+
+ + + Rivet for Agents + + + + The Stateful Runtime for
+ + AI Agents. + +
+ + + LLMs are stateless. Agents shouldn't be. Rivet Actors provide the persistent + memory, tool execution environment, and long-running context your agents need to + thrive. + + + + + + +
+ +
+ +
+ { + c.state.history.push({ role: 'user', content: message }); + + // Stream thought process + const result = await streamText({ + model: openai('gpt-4-turbo'), + messages: c.state.history, + onChunk: (chunk) => c.broadcast("delta", chunk) + }); + + // Automatically save context + c.state.history.push({ role: 'assistant', content: result.text }); + return result.text; + } + } +});`} + /> + +
+
+
+
+); + +const MemoryArchitecture = () => { + // Animation Step State + const [step, setStep] = useState(0); + + useEffect(() => { + const interval = setInterval(() => setStep((s) => (s + 1) % 3), 2500); + return () => clearInterval(interval); + }, []); + + return ( +
+
+
+ + Why Actors for Agents? + + + Traditional architectures force you to fetch conversation history from a database + for every single token generated. Rivet Actors keep the context hot in RAM. + +
+ +
+ {/* Diagram */} +
+ {/* The Loop */} +
+ {/* User */} +
+
+ +
+ User +
+ + {/* Flow Arrow */} +
+
+
+ + {/* The Actor (Memory + Compute) */} +
+
+ +
+ Agent Actor +
+ (Hot Context) +
+ + {/* Tool Call Bubble */} +
+ Thinking... +
+
+ + {/* Flow Arrow */} +
+
+
+ + {/* Tool/Output */} +
+
+ +
+ Tool +
+
+
+ + {/* Feature List */} +
+
+

+ + Zero-Latency Context +

+

+ Conversation history and embedding vectors stay in the Actor's heap. No + database queries required to "rehydrate" the agent state for each message. +

+
+
+
+

+ + Long-Running "Thought" Loops +

+

+ Agents often need to chain multiple tool calls (CoT). Actors can run for + minutes or hours, maintaining their state throughout the entire reasoning + chain without timeouts. +

+
+
+
+

+ + Multi-User Collaboration +

+

+ Since the Actor is a live process, multiple users can connect to the same + Agent instance simultaneously via WebSockets to collaborate or monitor + execution. +

+
+
+
+
+
+ ); +}; + +const AgentCapabilities = () => { + const capabilities = [ + { + title: "Streaming by Default", + description: + "Native support for SSE and WebSocket streaming. Pipe tokens from the LLM directly to the client with zero overhead.", + icon: Wifi, + color: "blue", + }, + { + title: "Tool Execution Sandbox", + description: + "Actors provide a secure isolation boundary. Run Python scripts or API calls without blocking your main API fleet.", + icon: Terminal, + color: "orange", + }, + { + title: "Human-in-the-Loop", + description: + "Pause execution, wait for a human approval signal via RPC, and then resume the agent's context exactly where it left off.", + icon: Users, + color: "purple", + }, + { + title: "Scheduled Wake-ups", + description: + "Set an alarm for your agent. It can sleep to disk and wake up in 2 days to follow up with a user automatically.", + icon: Clock, + color: "emerald", + }, + { + title: "Knowledge Graph State", + description: + "Keep a complex graph of entities in memory. Update relationships dynamically as the conversation progresses.", + icon: Network, + color: "blue", + }, + { + title: "Vendor Neutral", + description: + "Swap between OpenAI, Anthropic, or local Llama models instantly. The Actor pattern abstracts the underlying intelligence.", + icon: Globe, + color: "orange", + }, + ]; + + return ( +
+
+
+ + Built for the Agentic Future + + + The infrastructure primitives you need to move beyond simple chatbots. + +
+ +
+ {capabilities.map((cap, idx) => ( + + + + ))} +
+
+
+ ); +}; + +const UseCases = () => ( +
+
+
+
+ +
+ + Case Study +
+

+ Customer Support Swarms +

+

+ Deploy a dedicated Actor for every single active support ticket. +

+
    + {[ + "Isolation: One crashed agent doesn't affect others", + "Context: Full ticket history in memory (up to 128k tokens)", + "Handoff: Transfer actor state to a human instantly", + ].map((item, i) => ( +
  • +
    + +
    + {item} +
  • + ))} +
+
+
+ +
+
+
+
+
+ +
+
+
Ticket #9402
+
Agent: Active
+
+
+
+ Online +
+
+
+
+ Analysing user request... accessing database... +
+
+ Found 3 matching orders. Asking clarification... +
+
+
+
+
+
+
+
+ +
+
+
+); + +const OrchestrationSection = () => { + return ( +
+
+
+
+
+
+ { + // 1. Spawn specialized workers + const researcher = await c.spawn(researchAgent); + const coder = await c.spawn(codingAgent); + + // 2. Parallel execution (RPC) + const [context, code] = await Promise.all([ + researcher.rpc.gatherInfo(goal), + coder.rpc.generate(goal) + ]); + + // 3. Synthesize results + return { context, code, status: 'complete' }; + } + } +});`} + /> +
+
+ +
+ +
+ + Multi-Agent Systems +
+

+ Orchestrate
+ Agent Swarms. +

+

+ Don't stop at one. Build hierarchical trees of Actors where managers delegate + tasks to specialized workers. Rivet handles the supervision, networking, and + message passing between them automatically. +

+ +
+ {[ + { title: "Parallel Processing", desc: "Run 100 research agents at once." }, + { + title: "Supervision Trees", + desc: "Restart workers if they hallucinate or crash.", + }, + { + title: "Shared State", + desc: "Pass context references instantly between actors.", + }, + { + title: "Event Bus", + desc: "Pub/Sub messaging between swarm members.", + }, + ].map((item, i) => ( +
+

{item.title}

+

{item.desc}

+
+ ))} +
+
+
+
+
+
+ ); +}; + +const Ecosystem = () => ( +
+
+ + Works with your stack + +
+ {["LangChain", "LlamaIndex", "Vercel AI SDK", "OpenAI", "Anthropic", "HuggingFace"].map( + (tech) => ( +
+ {tech} +
+ ), + )} +
+
+
+); + +export default function AgentPage() { + return ( + +
+
+ + + + + + + + {/* CTA Section */} +
+
+ + Stop building generic bots. + + + Start building stateful, durable, intelligent agents that actually work. + + + + + +
+
+
+
+
+ ); +} + diff --git a/website/src/app/(v2)/(marketing)/solutions/agents/page.tsx b/website/src/app/(v2)/(marketing)/solutions/agents/page.tsx new file mode 100644 index 0000000000..463f076a6f --- /dev/null +++ b/website/src/app/(v2)/(marketing)/solutions/agents/page.tsx @@ -0,0 +1,747 @@ +"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, + Brain, + Sparkles, + Network, +} from "lucide-react"; +import { motion } from "framer-motion"; + +// --- Shared Design Components --- +const Badge = ({ text, color = "orange" }) => { + 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", + }; + + return ( +
+ + {text} +
+ ); +}; + +const CodeBlock = ({ code, fileName = "agent.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", "let", "var"].includes(trimmed)) { + tokens.push({part}); + } + // Functions & Special Rivet Terms + else if (["actor", "broadcast", "streamText", "spawn", "rpc"].includes(trimmed)) { + tokens.push({part}); + } + // Object Keys / Properties / Methods + else if (["state", "actions", "history", "ticketId", "chat", "message", "model", "messages", "onChunk", "delta", "role", "content", "text", "subtasks", "runWorkflow", "goal", "researcher", "coder", "gatherInfo", "generate", "context", "code", "status", "complete"].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; + })()} + +
+ ))} +
+
+
+
+ ); +}; + +// --- Refined Agent Card matching landing page style with color highlights --- +const SolutionCard = ({ title, description, icon: Icon, color = "orange" }) => { + const getColorClasses = (col) => { + switch (col) { + case "orange": + return { + bg: "bg-orange-500/10", + text: "text-orange-400", + hoverBg: "group-hover:bg-orange-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(249,115,22,0.5)]", + border: "border-orange-500", + glow: "rgba(249,115,22,0.15)", + }; + case "blue": + return { + bg: "bg-blue-500/10", + text: "text-blue-400", + hoverBg: "group-hover:bg-blue-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(59,130,246,0.5)]", + border: "border-blue-500", + glow: "rgba(59,130,246,0.15)", + }; + case "purple": + return { + bg: "bg-purple-500/10", + text: "text-purple-400", + hoverBg: "group-hover:bg-purple-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(168,85,247,0.5)]", + border: "border-purple-500", + glow: "rgba(168,85,247,0.15)", + }; + case "emerald": + return { + bg: "bg-emerald-500/10", + text: "text-emerald-400", + hoverBg: "group-hover:bg-emerald-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(16,185,129,0.5)]", + border: "border-emerald-500", + glow: "rgba(16,185,129,0.15)", + }; + default: + return { + bg: "bg-orange-500/10", + text: "text-orange-400", + hoverBg: "group-hover:bg-orange-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(249,115,22,0.5)]", + border: "border-orange-500", + glow: "rgba(249,115,22,0.15)", + }; + } + }; + const c = getColorClasses(color); + + return ( +
+ {/* Top Shine Highlight */} +
+ + {/* Top Left Reflection/Glow */} +
+ {/* Sharp Edge Highlight (Masked) */} +
+ +
+
+
+
+ +
+

{title}

+
+
+

{description}

+
+
+ ); +}; + +// --- Page Sections --- +const Hero = () => ( +
+
+ +
+
+
+ + + + Build
+ AI Agents. +
+ + + LLMs are stateless. Agents shouldn't be. Rivet Actors provide the persistent memory, tool execution environment, and long-running context your agents need to thrive. + + + + +
+
+
+
+ { + c.state.history.push({ role: 'user', content: message }); + + // Stream thought process + const result = await streamText({ + model: openai('gpt-4-turbo'), + messages: c.state.history, + onChunk: (chunk) => c.broadcast("delta", chunk) + }); + // Automatically save context + c.state.history.push({ role: 'assistant', content: result.text }); + return result.text; + } + } +});`} + /> +
+
+
+
+
+); + +const MemoryArchitecture = () => { + // Animation Step State + const [step, setStep] = useState(0); + + useEffect(() => { + const interval = setInterval(() => setStep((s) => (s + 1) % 3), 2500); + return () => clearInterval(interval); + }, []); + + return ( +
+
+
+ + Why Actors for Agents? + + + Traditional architectures force you to fetch conversation history from a database for every single token generated. Rivet Actors keep the context hot in RAM. + +
+ +
+ {/* Diagram */} +
+ {/* The Loop */} +
+ {/* User */} +
+
+ +
+ User +
+ + {/* Flow Arrow */} +
+
+
+ + {/* The Actor (Memory + Compute) */} +
+
+ +
+ Agent Actor +
+ (Hot Context) +
+ + {/* Tool Call Bubble */} +
+ Thinking... +
+
+ {/* Flow Arrow */} +
+
+
+ {/* Tool/Output */} +
+
+ +
+ Tool +
+
+
+ + {/* Feature List */} +
+ +

+ + Zero-Latency Context +

+

+ Conversation history and embedding vectors stay in the Actor's heap. No database queries required to "rehydrate" the agent state for each message. +

+
+
+ +

+ + Long-Running "Thought" Loops +

+

+ Agents often need to chain multiple tool calls (CoT). Actors can run for minutes or hours, maintaining their state throughout the entire reasoning chain without timeouts. +

+
+
+ +

+ + Multi-User Collaboration +

+

+ Since the Actor is a live process, multiple users can connect to the same Agent instance simultaneously via WebSockets to collaborate or monitor execution. +

+
+
+
+
+
+ ); +}; + +const AgentCapabilities = () => { + const capabilities = [ + { + title: "Streaming by Default", + description: "Native support for SSE and WebSocket streaming. Pipe tokens from the LLM directly to the client with zero overhead.", + icon: Wifi, + color: "blue", + }, + { + title: "Tool Execution Sandbox", + description: "Actors provide a secure isolation boundary. Run Python scripts or API calls without blocking your main API fleet.", + icon: Terminal, + color: "orange", + }, + { + title: "Human-in-the-Loop", + description: "Pause execution, wait for a human approval signal via RPC, and then resume the agent's context exactly where it left off.", + icon: Users, + color: "purple", + }, + { + title: "Scheduled Wake-ups", + description: "Set an alarm for your agent. It can sleep to disk and wake up in 2 days to follow up with a user automatically.", + icon: Clock, + color: "emerald", + }, + { + title: "Knowledge Graph State", + description: "Keep a complex graph of entities in memory. Update relationships dynamically as the conversation progresses.", + icon: Network, + color: "blue", + }, + { + title: "Vendor Neutral", + description: "Swap between OpenAI, Anthropic, or local Llama models instantly. The Actor pattern abstracts the underlying intelligence.", + icon: Globe, + color: "orange", + }, + ]; + + return ( +
+
+ +

Built for the Agentic Future

+

The infrastructure primitives you need to move beyond simple chatbots.

+
+ +
+ {capabilities.map((cap, idx) => ( + + + + ))} +
+
+
+ ); +}; + +const UseCases = () => ( +
+
+
+
+ + + Customer Support Swarms + + + Deploy a dedicated Actor for every single active support ticket. + +
    + {["Isolation: One crashed agent doesn't affect others", "Context: Full ticket history in memory (up to 128k tokens)", "Handoff: Transfer actor state to a human instantly"].map((item, i) => ( + +
    + +
    + {item} +
    + ))} +
+
+ +
+
+
+
+
+ +
+
+
Ticket #9402
+
Agent: Active
+
+
+
Online
+
+
+
Analysing user request... accessing database...
+
Found 3 matching orders. Asking clarification...
+
+
+
+
+
+
+
+ +
+
+
+); + +const OrchestrationSection = () => { + return ( +
+
+
+
+
+
+ { + // 1. Spawn specialized workers + const researcher = await c.spawn(researchAgent); + const coder = await c.spawn(codingAgent); + + // 2. Parallel execution (RPC) + const [context, code] = await Promise.all([ + researcher.rpc.gatherInfo(goal), + coder.rpc.generate(goal) + ]); + + // 3. Synthesize results + return { context, code, status: 'complete' }; + } + } +});`} + /> +
+
+
+ + + Orchestrate
+ Agent Swarms. +
+ + Don't stop at one. Build hierarchical trees of Actors where managers delegate tasks to specialized workers. Rivet handles the supervision, networking, and message passing between them automatically. + +
+ {[ + { title: "Parallel Processing", desc: "Run 100 research agents at once." }, + { title: "Supervision Trees", desc: "Restart workers if they hallucinate or crash." }, + { title: "Shared State", desc: "Pass context references instantly between actors." }, + { title: "Event Bus", desc: "Pub/Sub messaging between swarm members." }, + ].map((item, i) => ( + +

{item.title}

+

{item.desc}

+
+ ))} +
+
+
+
+
+ ); +}; + +const Ecosystem = () => ( +
+
+ + Works with your stack + +
+ {["LangChain", "LlamaIndex", "Vercel AI SDK", "OpenAI", "Anthropic", "HuggingFace"].map((tech, i) => ( + + {tech} + + ))} +
+
+
+); + +export default function AgentsPage() { + return ( +
+
+ + + + + + + + {/* CTA Section */} +
+
+ + Stop building generic bots. + + + Start building stateful, durable, intelligent agents that actually work. + + + + + +
+
+
+
+ ); +} + diff --git a/website/src/app/(v2)/(marketing)/solutions/collaborative-state/page.tsx b/website/src/app/(v2)/(marketing)/solutions/collaborative-state/page.tsx new file mode 100644 index 0000000000..20ce09b027 --- /dev/null +++ b/website/src/app/(v2)/(marketing)/solutions/collaborative-state/page.tsx @@ -0,0 +1,675 @@ +"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, + Brain, + Sparkles, + Network, + Calendar, + GitBranch, + Timer, + Mail, + CreditCard, + Bell, + Server, + Sword, + Trophy, + Target, + MousePointer2, + Share2, + Edit3, +} from "lucide-react"; +import { motion } from "framer-motion"; + +// --- Shared Design Components --- +const Badge = ({ text, color = "blue" }) => { + 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", + green: "text-green-400 border-green-500/20 bg-green-500/10", + }; + + return ( +
+ + {text} +
+ ); +}; + +const CodeBlock = ({ code, fileName = "room.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", "let", "var", "if", "else"].includes(trimmed)) { + tokens.push({part}); + } + // Functions & Special Rivet Terms + else if (["actor", "broadcast", "spawn", "rpc", "applyPatch", "connectionId"].includes(trimmed)) { + tokens.push({part}); + } + // Object Keys / Properties / Methods + else if (["state", "actions", "content", "cursors", "update", "patch", "moveCursor", "x", "y", "presence"].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; + })()} + +
+ ))} +
+
+
+
+ ); +}; + +// --- Refined Collaboration Card matching landing page style with color highlights --- +const SolutionCard = ({ title, description, icon: Icon, color = "blue" }) => { + const getColorClasses = (col) => { + switch (col) { + case "orange": + return { + bg: "bg-orange-500/10", + text: "text-orange-400", + hoverBg: "group-hover:bg-orange-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(249,115,22,0.5)]", + border: "border-orange-500", + glow: "rgba(249,115,22,0.15)", + }; + case "blue": + return { + bg: "bg-blue-500/10", + text: "text-blue-400", + hoverBg: "group-hover:bg-blue-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(59,130,246,0.5)]", + border: "border-blue-500", + glow: "rgba(59,130,246,0.15)", + }; + case "purple": + return { + bg: "bg-purple-500/10", + text: "text-purple-400", + hoverBg: "group-hover:bg-purple-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(168,85,247,0.5)]", + border: "border-purple-500", + glow: "rgba(168,85,247,0.15)", + }; + case "emerald": + return { + bg: "bg-emerald-500/10", + text: "text-emerald-400", + hoverBg: "group-hover:bg-emerald-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(16,185,129,0.5)]", + border: "border-emerald-500", + glow: "rgba(16,185,129,0.15)", + }; + case "green": + return { + bg: "bg-green-500/10", + text: "text-green-400", + hoverBg: "group-hover:bg-green-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(34,197,94,0.5)]", + border: "border-green-500", + glow: "rgba(34,197,94,0.15)", + }; + default: + return { + bg: "bg-blue-500/10", + text: "text-blue-400", + hoverBg: "group-hover:bg-blue-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(59,130,246,0.5)]", + border: "border-blue-500", + glow: "rgba(59,130,246,0.15)", + }; + } + }; + const c = getColorClasses(color); + + return ( +
+ {/* Top Shine Highlight */} +
+ + {/* Top Left Reflection/Glow */} +
+ {/* Sharp Edge Highlight (Masked) */} +
+ +
+
+
+
+ +
+

{title}

+
+
+

{description}

+
+
+ ); +}; + +// --- Page Sections --- +const Hero = () => ( +
+
+ +
+
+
+ + + + Multiplayer by
+ Default. +
+ + + Stop managing WebSocket fleets. Rivet Actors give you instant, stateful rooms for collaborative documents, whiteboards, and chat. + + + + +
+
+
+
+ { + c.state.content = applyPatch(c.state.content, patch); + + // Broadcast to all other clients in room + c.broadcast("patch", patch); + }, + + // Ephemeral state for presence + moveCursor: (c, { x, y }) => { + c.state.cursors[c.connectionId] = { x, y }; + c.broadcast("presence", c.state.cursors); + } + } +});`} + /> +
+
+
+
+
+); + +const RoomArchitecture = () => { + const [cursor1, setCursor1] = useState({ x: 30, y: 40 }); + const [cursor2, setCursor2] = useState({ x: 70, y: 60 }); + + useEffect(() => { + const interval = setInterval(() => { + setCursor1({ x: 30 + Math.sin(Date.now() / 1000) * 20, y: 40 + Math.cos(Date.now() / 1000) * 10 }); + setCursor2({ x: 70 + Math.cos(Date.now() / 800) * 15, y: 60 + Math.sin(Date.now() / 800) * 15 }); + }, 50); + return () => clearInterval(interval); + }, []); + + return ( +
+
+
+ + Room-Based Architecture + + + Every document or session gets its own dedicated Actor. This isolates state, prevents database contention, and guarantees order of operations. + +
+ +
+ {/* Interactive Diagram */} +
+ {/* The Room (Actor) */} +
+
ACTOR: room-8392
+ + {/* Simulated Doc */} +
+
+
+
+
+
+ {/* Virtual Cursors */} +
+ +
User A
+
+
+ +
User B
+
+
+
+ + {/* Connection Lines (Decorative) */} +
+
+
+ + {/* Feature List */} +
+ +

+ + Instant Presence +

+

+ Broadcast cursor positions and selection states to 100+ users in the same room with <10ms latency. +

+
+
+ +

+ + Authoritative State +

+

+ The Actor holds the "source of truth" in memory. Resolve conflicts on the server or relay operations for client-side CRDT merging. +

+
+
+ +

+ + Connection Limits +

+

+ Automatically scales to handle thousands of concurrent active rooms. Each room hibernates when the last user leaves. +

+
+
+
+
+
+ ); +}; + +const CollaborationFeatures = () => { + const features = [ + { + title: "WebSockets Included", + description: "No need for Pusher or separate socket servers. Actors speak WebSocket natively. Just connect() and listen.", + icon: Wifi, + color: "green", + }, + { + title: "Broadcast & Pub/Sub", + description: "Send a message to everyone in the room, or target specific users. Built-in channels for effortless event routing.", + icon: Share2, + color: "blue", + }, + { + title: "Ephemeral Storage", + description: "Perfect for 'who is typing' indicators or selection highlights that don't need to be saved to the database.", + icon: Zap, + color: "orange", + }, + { + title: "Conflict Resolution", + description: "Run logic on the server to validate moves or merge edits before they are broadcast to other players.", + icon: Workflow, + color: "purple", + }, + { + title: "History & Replay", + description: "Keep a running log of actions in memory. Allow users to undo/redo or replay the session history.", + icon: RefreshCw, + color: "blue", + }, + { + title: "Yjs & Automerge", + description: "A perfect host for CRDT backends. Store the encoded document state in the Actor and sync changes effortlessly.", + icon: FileText, + color: "green", + }, + ]; + + return ( +
+
+
+ + The Engine for Collaboration + + + Primitives designed for high-concurrency, low-latency interactive apps. + +
+ +
+ {features.map((feat, idx) => ( + + + + ))} +
+
+
+ ); +}; + +const UseCases = () => ( +
+
+
+
+ + + Infinite Canvases + + + Build the next Figma or Miro. Store thousands of vector objects in memory and stream updates only for the viewport. + + + {[ + "Spatial Indexing: Query objects by x/y coordinates", + "Delta Compression: Only send changed attributes", + "Locking: Prevent two users from moving the same object", + ].map((item, i) => ( +
  • +
    + +
    + {item} +
  • + ))} +
    +
    + +
    +
    + {/* Grid Pattern */} +
    + + {/* Shapes */} +
    +
    + + {/* Cursor interacting */} +
    + +
    Sarah
    +
    +
    + +
    +
    +
    +); + +const Ecosystem = () => ( +
    +
    + + Integrates with + +
    + {["Y.js", "Automerge", "Prosemirror", "Tldraw", "Excalidraw", "Liveblocks Client"].map((tech, i) => ( + + {tech} + + ))} +
    +
    +
    +); + +export default function CollaborativeStatePage() { + return ( +
    +
    + + + + + + + {/* CTA Section */} +
    +
    + + Ready to go multiplayer? + + + Build the collaborative features your users expect, without the infrastructure headache. + + + + + +
    +
    +
    +
    + ); +} + diff --git a/website/src/app/(v2)/(marketing)/solutions/games/page.tsx b/website/src/app/(v2)/(marketing)/solutions/games/page.tsx new file mode 100644 index 0000000000..c974710c9b --- /dev/null +++ b/website/src/app/(v2)/(marketing)/solutions/games/page.tsx @@ -0,0 +1,704 @@ +"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, + Brain, + Sparkles, + Network, + 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", "let", "var", "if", "else"].includes(trimmed)) { + tokens.push({part}); + } + // Functions & Special Rivet Terms + else if (["actor", "broadcast", "spawn", "rpc", "connectionId"].includes(trimmed)) { + tokens.push({part}); + } + // Object Keys / Properties / Methods + else if (["state", "actions", "players", "scores", "map", "join", "move", "name", "hp", "pos", "x", "y", "isValidMove"].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; + })()} + +
    + ))} +
    +
    +
    +
    + ); +}; + +// --- Refined Game Card matching landing page style with color highlights --- +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", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(249,115,22,0.5)]", + border: "border-orange-500", + glow: "rgba(249,115,22,0.15)", + }; + case "blue": + return { + bg: "bg-blue-500/10", + text: "text-blue-400", + hoverBg: "group-hover:bg-blue-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(59,130,246,0.5)]", + border: "border-blue-500", + glow: "rgba(59,130,246,0.15)", + }; + case "purple": + return { + bg: "bg-purple-500/10", + text: "text-purple-400", + hoverBg: "group-hover:bg-purple-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(168,85,247,0.5)]", + border: "border-purple-500", + glow: "rgba(168,85,247,0.15)", + }; + case "emerald": + return { + bg: "bg-emerald-500/10", + text: "text-emerald-400", + hoverBg: "group-hover:bg-emerald-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(16,185,129,0.5)]", + border: "border-emerald-500", + glow: "rgba(16,185,129,0.15)", + }; + case "red": + return { + bg: "bg-red-500/10", + text: "text-red-400", + hoverBg: "group-hover:bg-red-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(239,68,68,0.5)]", + border: "border-red-500", + glow: "rgba(239,68,68,0.15)", + }; + default: + return { + bg: "bg-red-500/10", + text: "text-red-400", + hoverBg: "group-hover:bg-red-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(239,68,68,0.5)]", + border: "border-red-500", + glow: "rgba(239,68,68,0.15)", + }; + } + }; + const c = getColorClasses(color); + + return ( +
    + {/* Top Shine Highlight */} +
    + + {/* Top Left Reflection/Glow */} +
    + {/* Sharp Edge Highlight (Masked) */} +
    + +
    +
    +
    +
    + +
    +

    {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 P1 (Odd ticks: visible at start, moves right) */} +
    +
    + + {/* Client 2 */} +
    +
    + +
    + Player 2 + {/* Incoming Input P2 (Even ticks: visible at start, moves left) */} +
    +
    +
    + + {/* Server Console */} +
    +
    server.tick({tick - 1})
    +
    server.tick({tick}) > Broadcasting State Snapshot
    +
    + {tick % 2 !== 0 ? ( + < P1 Input: Move(x: 12, y: 40) + ) : ( + < P2 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 GamesPage() { + 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)/(marketing)/solutions/user-session-store/page.tsx b/website/src/app/(v2)/(marketing)/solutions/user-session-store/page.tsx new file mode 100644 index 0000000000..bb06e9a0ee --- /dev/null +++ b/website/src/app/(v2)/(marketing)/solutions/user-session-store/page.tsx @@ -0,0 +1,717 @@ +"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, + Brain, + Sparkles, + Network, + Calendar, + GitBranch, + Timer, + Mail, + CreditCard, + Bell, + Server, + Sword, + Trophy, + Target, + Fingerprint, + Cookie, + Lock, + Smartphone, + ShoppingCart, + Shield, +} from "lucide-react"; +import { motion } from "framer-motion"; + +// --- Shared Design Components --- +const Badge = ({ text, color = "cyan" }) => { + 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", + cyan: "text-cyan-400 border-cyan-500/20 bg-cyan-500/10", + }; + + return ( +
    + + {text} +
    + ); +}; + +const CodeBlock = ({ code, fileName = "session.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", "let", "var", "if", "else"].includes(trimmed)) { + tokens.push({part}); + } + // Functions & Special Rivet Terms + else if (["actor", "broadcast", "spawn", "rpc", "schedule", "token", "generate"].includes(trimmed)) { + tokens.push({part}); + } + // Object Keys / Properties / Methods + else if (["state", "actions", "user", "cart", "preferences", "theme", "login", "userData", "addToCart", "item", "push", "destroy"].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; + })()} + +
    + ))} +
    +
    +
    +
    + ); +}; + +// --- Refined Session Card matching landing page style with color highlights --- +const SolutionCard = ({ title, description, icon: Icon, color = "cyan" }) => { + const getColorClasses = (col) => { + switch (col) { + case "orange": + return { + bg: "bg-orange-500/10", + text: "text-orange-400", + hoverBg: "group-hover:bg-orange-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(249,115,22,0.5)]", + border: "border-orange-500", + glow: "rgba(249,115,22,0.15)", + }; + case "blue": + return { + bg: "bg-blue-500/10", + text: "text-blue-400", + hoverBg: "group-hover:bg-blue-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(59,130,246,0.5)]", + border: "border-blue-500", + glow: "rgba(59,130,246,0.15)", + }; + case "purple": + return { + bg: "bg-purple-500/10", + text: "text-purple-400", + hoverBg: "group-hover:bg-purple-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(168,85,247,0.5)]", + border: "border-purple-500", + glow: "rgba(168,85,247,0.15)", + }; + case "emerald": + return { + bg: "bg-emerald-500/10", + text: "text-emerald-400", + hoverBg: "group-hover:bg-emerald-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(16,185,129,0.5)]", + border: "border-emerald-500", + glow: "rgba(16,185,129,0.15)", + }; + case "red": + return { + bg: "bg-red-500/10", + text: "text-red-400", + hoverBg: "group-hover:bg-red-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(239,68,68,0.5)]", + border: "border-red-500", + glow: "rgba(239,68,68,0.15)", + }; + case "cyan": + return { + bg: "bg-cyan-500/10", + text: "text-cyan-400", + hoverBg: "group-hover:bg-cyan-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(34,211,238,0.5)]", + border: "border-cyan-500", + glow: "rgba(34,211,238,0.15)", + }; + default: + return { + bg: "bg-cyan-500/10", + text: "text-cyan-400", + hoverBg: "group-hover:bg-cyan-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(34,211,238,0.5)]", + border: "border-cyan-500", + glow: "rgba(34,211,238,0.15)", + }; + } + }; + const c = getColorClasses(color); + + return ( +
    + {/* Top Shine Highlight */} +
    + + {/* Top Left Reflection/Glow */} +
    + {/* Sharp Edge Highlight (Masked) */} +
    + +
    +
    +
    +
    + +
    +

    {title}

    +
    +
    +

    {description}

    +
    +
    + ); +}; + +// --- Page Sections --- +const Hero = () => ( +
    +
    + +
    +
    +
    + + + + Session State.
    + Instantly Available. +
    + + + Replace Redis and cookies with persistent Actors. Attach rich, real-time state to every user, instantly accessible from the edge without database latency. + + + + +
    +
    +
    +
    + { + c.state.user = userData; + // Set expiry for inactivity (30 mins) + c.schedule("destroy", "30m"); + return c.token.generate(); + }, + + addToCart: (c, item) => { + c.state.cart.push(item); + // Broadcast update to all user's tabs + c.broadcast("cart_updated", c.state.cart); + } + } +});`} + /> +
    +
    +
    +
    +
    +); + +const LatencyArchitecture = () => { + const [step, setStep] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + setStep((s) => (s + 1) % 4); + }, 1500); + return () => clearInterval(interval); + }, []); + + return ( +
    +
    +
    + + Zero-Latency Profile Access + + + Traditional architectures require a database round-trip for every request to fetch user sessions. Rivet Actors keep the user's state hot in memory at the edge. + +
    + +
    + {/* Visualization */} +
    +
    + {/* User */} +
    +
    + +
    + Request +
    + + {/* Path */} +
    + {/* Packet */} +
    +
    +
    + + {/* The Session Actor */} +
    +
    + +
    + Session Actor + {step === 2 && ( +
    + Memory Hit +
    + )} +
    +
    + + {/* Comparison text */} +
    +
    +
    + Postgres: 45ms +
    +
    +
    + Rivet: 2ms +
    +
    +
    + + {/* Feature List */} +
    + +

    + + Instant Read/Write +

    +

    + State is stored in RAM. Reading user preferences or checking permissions happens in microseconds, not milliseconds. +

    +
    +
    + +

    + + Edge Routing +

    +

    + Actors are automatically instantiated in the region closest to the user, ensuring global low-latency access. +

    +
    +
    + +

    + + Secure by Default +

    +

    + Isolated memory spaces for every user. No risk of cross-tenant data leaks or cache poisoning. +

    +
    +
    +
    +
    +
    + ); +}; + +const SessionFeatures = () => { + const features = [ + { + title: "Live Sync", + description: "Changes to session data (e.g. dark mode toggle) are broadcast instantly to all open tabs or devices via WebSockets.", + icon: Wifi, + color: "cyan", + }, + { + title: "TTL & Expiry", + description: "Automatically destroy session actors after a period of inactivity to free up resources and enforce security.", + icon: Clock, + color: "orange", + }, + { + title: "Cart Persistence", + description: "Never lose a shopping cart item again. State survives server restarts and follows the user across devices.", + icon: ShoppingCart, + color: "emerald", + }, + { + title: "Presence", + description: "Know exactly when a user is online. The Actor exists only while the user is connected (or for a grace period).", + icon: Activity, + color: "blue", + }, + { + title: "Auth Tokens", + description: "Generate and validate JWTs directly within the Actor. Revoke tokens instantly by killing the Actor.", + icon: Shield, + color: "red", + }, + { + title: "Device Handover", + description: "Start a task on mobile, finish on desktop. The shared Actor state creates a seamless continuity.", + icon: Smartphone, + color: "purple", + }, + ]; + + return ( +
    +
    +
    + + More than just a Cookie + + + Turn your passive session store into an active engine for user experience. + +
    + +
    + {features.map((feat, idx) => ( + + + + ))} +
    +
    +
    + ); +}; + +const UseCases = () => ( +
    +
    +
    +
    + + + Real-time E-Commerce + + + A high-volume storefront where cart inventory is reserved instantly and stock levels update live. + + + {[ + "Inventory Locks: Items reserved in Actor memory", + "Dynamic Pricing: Personalized discounts applied instantly", + "Cross-Device: Cart updates on phone appear on laptop", + ].map((item, i) => ( +
  • +
    + +
    + {item} +
  • + ))} +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    User: alex_92
    +
    Session Active (2 devices)
    +
    +
    +
    Online
    +
    +
    +
    + Item Added: Mechanical Keyboard + +$149.00 +
    +
    + Broadcast: "cart_update" -> 2 clients +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +); + +const Ecosystem = () => ( +
    +
    + + Works with your auth provider + +
    + {["Clerk", "Auth0", "Supabase Auth", "NextAuth.js", "Stytch", "Firebase"].map((tech, i) => ( + + {tech} + + ))} +
    +
    +
    +); + +export default function UserSessionStorePage() { + return ( +
    +
    + + + + + + + {/* CTA Section */} +
    +
    + + Stop querying the database. + + + Move your user state to the edge with Rivet Actors. + + + + + +
    +
    +
    +
    + ); +} + diff --git a/website/src/app/(v2)/(marketing)/solutions/workflows/page.tsx b/website/src/app/(v2)/(marketing)/solutions/workflows/page.tsx new file mode 100644 index 0000000000..1ab2835e87 --- /dev/null +++ b/website/src/app/(v2)/(marketing)/solutions/workflows/page.tsx @@ -0,0 +1,705 @@ +"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, + Brain, + Sparkles, + Network, + Calendar, + GitBranch, + Timer, + Mail, + CreditCard, + Bell, +} from "lucide-react"; +import { motion } from "framer-motion"; + +// --- Shared Design Components --- +const Badge = ({ text, color = "emerald" }) => { + 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", + }; + + return ( +
    + + {text} +
    + ); +}; + +const CodeBlock = ({ code, fileName = "workflow.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", "let", "var", "if", "else"].includes(trimmed)) { + tokens.push({part}); + } + // Functions & Special Rivet Terms + else if (["actor", "broadcast", "sleep", "waitForSignal", "spawn", "rpc"].includes(trimmed)) { + tokens.push({part}); + } + // Object Keys / Properties / Methods + else if (["state", "actions", "step", "userId", "start", "email", "send", "hasLoggedIn", "subtasks", "runWorkflow", "goal", "researcher", "coder", "gatherInfo", "generate", "context", "code", "status", "complete"].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; + })()} + +
    + ))} +
    +
    +
    +
    + ); +}; + +// --- Refined Workflow Card matching landing page style with color highlights --- +const SolutionCard = ({ title, description, icon: Icon, color = "emerald" }) => { + const getColorClasses = (col) => { + switch (col) { + case "orange": + return { + bg: "bg-orange-500/10", + text: "text-orange-400", + hoverBg: "group-hover:bg-orange-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(249,115,22,0.5)]", + border: "border-orange-500", + glow: "rgba(249,115,22,0.15)", + }; + case "blue": + return { + bg: "bg-blue-500/10", + text: "text-blue-400", + hoverBg: "group-hover:bg-blue-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(59,130,246,0.5)]", + border: "border-blue-500", + glow: "rgba(59,130,246,0.15)", + }; + case "purple": + return { + bg: "bg-purple-500/10", + text: "text-purple-400", + hoverBg: "group-hover:bg-purple-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(168,85,247,0.5)]", + border: "border-purple-500", + glow: "rgba(168,85,247,0.15)", + }; + case "emerald": + return { + bg: "bg-emerald-500/10", + text: "text-emerald-400", + hoverBg: "group-hover:bg-emerald-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(16,185,129,0.5)]", + border: "border-emerald-500", + glow: "rgba(16,185,129,0.15)", + }; + default: + return { + bg: "bg-emerald-500/10", + text: "text-emerald-400", + hoverBg: "group-hover:bg-emerald-500/20", + hoverShadow: "group-hover:shadow-[0_0_15px_rgba(16,185,129,0.5)]", + border: "border-emerald-500", + glow: "rgba(16,185,129,0.15)", + }; + } + }; + const c = getColorClasses(color); + + return ( +
    + {/* Top Shine Highlight */} +
    + + {/* Top Left Reflection/Glow */} +
    + {/* Sharp Edge Highlight (Masked) */} +
    + +
    +
    +
    +
    + +
    +

    {title}

    +
    +
    +

    {description}

    +
    +
    + ); +}; + +// --- Page Sections --- +const Hero = () => ( +
    +
    + +
    +
    +
    + + + + Workflows that
    + Never Fail. +
    + + + Replace complex queues and state machines with simple code. Rivet Actors persist their execution state to disk, surviving server restarts and sleeping for months without resources. + + + + +
    +
    +
    +
    + { + // 1. Send welcome email + await c.email.send("welcome"); + + // 2. Hibernate for 3 days (0 cost) + await c.sleep("3d"); + + // 3. Wake up and check status + if (!c.state.hasLoggedIn) { + await c.email.send("nudge"); + // Wait for human signal or timeout + await c.waitForSignal("login", "7d"); + } + + return "completed"; + } + } +});`} + /> +
    +
    +
    +
    +
    +); + +const WorkflowArchitecture = () => { + const [activeDay, setActiveDay] = useState(1); + + useEffect(() => { + const interval = setInterval(() => { + setActiveDay((d) => (d >= 4 ? 1 : d + 1)); + }, 2000); + return () => clearInterval(interval); + }, []); + + return ( +
    +
    +
    + + The Sleep/Wake Cycle + + + Unlike standard cron jobs, Actors maintain their exact execution pointer and local variable state across sleeps. They don't restart from the beginning; they continue. + +
    + +
    + {/* Visualization */} +
    + + {/* Timeline Container */} +
    +
    + {/* Node 1: Start */} +
    +
    + +
    + Start +
    + + {/* Spacer 1: Sleep */} +
    + {/* Track Line */} +
    + + {/* Progress Line */} +
    = 2 ? "scale-x-100" : "scale-x-0"}`} /> + + {/* Label floating above */} +
    Hibernating
    +
    + + {/* Node 2: Resume */} +
    +
    + +
    + Resume +
    + + {/* Spacer 2: Short */} +
    +
    +
    = 4 ? "scale-x-100" : "scale-x-0"}`} /> +
    + + {/* Node 3: Done */} +
    +
    + +
    + Done +
    +
    +
    + + {/* Console Log Simulation */} +
    +
    +
    +
    +
    + workflow_logs.txt +
    +
    +
    = 1 ? "opacity-100" : "opacity-20"} transition-opacity`}> + [10:00:00] INFO: Workflow started. sending_email... +
    +
    = 2 ? "opacity-100" : "opacity-20"} transition-opacity`}> + [10:00:01] SLEEP: Hibernating for 3 days... +
    +
    = 3 ? "opacity-100" : "opacity-20"} transition-opacity`}> + [+3d 00:00] WAKE: Context restored from disk. +
    +
    = 4 ? "opacity-100" : "opacity-20"} transition-opacity`}> + [+3d 00:01] SUCCESS: User logged in. Completing. +
    +
    +
    +
    + + {/* Feature List */} +
    + +

    + + Implicit State +

    +

    + Forget UPDATE users SET status = 'emailed'. Just define a variable in your code. Rivet persists the entire JS closure automatically. +

    +
    +
    + +

    + + Zero-Cost Waiting +

    +

    + When you await sleep('1y'), the Actor serializes to disk. You pay absolutely nothing for compute while it waits. +

    +
    +
    + +

    + + Reliability Guarantees +

    +

    + If the server crashes or deploys during a sleep, the Actor wakes up on a healthy node as if nothing happened. +

    +
    +
    +
    +
    +
    + ); +}; + +const WorkflowFeatures = () => { + const features = [ + { + title: "Durable Timers", + description: "Schedule code to run in the future using natural language. '2 days', 'next friday', or specific ISO dates.", + icon: Calendar, + color: "emerald", + }, + { + title: "Human-in-the-Loop", + description: "Pause execution until an external signal is received. Perfect for approval flows or 2FA verifications.", + icon: Users, + color: "blue", + }, + { + title: "Scheduled Jobs (Cron)", + description: "Actors can be self-waking. Create a singleton actor that wakes up every hour to perform maintenance tasks.", + icon: Clock, + color: "orange", + }, + { + title: "Retries & Backoff", + description: "Wrap flaky API calls in robust retry logic. If the process crashes, it resumes exactly where it failed.", + icon: RefreshCw, + color: "purple", + }, + { + title: "Sub-Workflows", + description: "Spawn child actors to handle parallel tasks. The parent actor waits for results, aggregating data cleanly.", + icon: GitBranch, + color: "emerald", + }, + { + title: "State Inspection", + description: "Debug running workflows by inspecting their memory state in real-time via the dashboard or REPL.", + icon: Eye, + color: "blue", + }, + ]; + + return ( +
    +
    + +

    Primitive for Reliability

    +

    Building blocks for systems that must finish what they start.

    +
    + +
    + {features.map((feat, idx) => ( + + + + ))} +
    +
    +
    + ); +}; + +const UseCases = () => ( +
    +
    +
    +
    + + + Payment Dunning + + + Recover failed payments with a stateful actor that manages the entire lifecycle of the retry process. + +
      + {["Trigger: Stripe webhook spawns a DunningActor", "Logic: Wait 3 days, email user, retry charge", "End: If success, kill actor. If fail after 3 tries, cancel sub."].map((item, i) => ( + +
      + +
      + {item} +
      + ))} +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    Invoice #INV-2049
    +
    Status: Retrying (Attempt 2/3)
    +
    +
    +
    Pending
    +
    +
    +
    + Today + Next Retry: 2d +
    +
    +
    +
    +
    Card declined. Email sent to user@example.com. Sleeping...
    +
    +
    + +
    +
    +
    +); + +const Ecosystem = () => ( +
    +
    + + Connects with + +
    + {["Stripe", "Resend", "Twilio", "Slack", "Linear", "Postgres", "Discord"].map((tech, i) => ( + + {tech} + + ))} +
    +
    +
    +); + +export default function WorkflowsPage() { + return ( +
    +
    + + + + + + + {/* CTA Section */} +
    +
    + + Sleep well at night. + + + Trust your critical background processes to a runtime built for durability. + + + + + +
    +
    +
    +
    + ); +} + From 9e5381cb583723b73ff7fd792a8c1ffe415986f8 Mon Sep 17 00:00:00 2001 From: Nicholas Kissel Date: Wed, 19 Nov 2025 00:05:35 -0800 Subject: [PATCH 03/13] Update landing page sections and header with highlights, animations, and solutions dropdown --- .../(index)/sections/CodeWalkthrough.tsx | 22 +-- .../(index)/sections/ConceptSection.tsx | 42 +++--- .../(index)/sections/FeaturesSection.tsx | 20 ++- .../(index)/sections/HostingSection.tsx | 20 ++- .../(index)/sections/IntegrationsSection.tsx | 114 +++++++------- .../(index)/sections/ObservabilitySection.tsx | 14 +- .../(index)/sections/RedesignedCTA.tsx | 8 +- .../(index)/sections/RedesignedHero.tsx | 34 ++--- .../(index)/sections/SolutionSection.tsx | 8 +- .../(index)/sections/SolutionsSection.tsx | 10 +- .../app/(v2)/[section]/[[...page]]/page.tsx | 65 ++++++-- website/src/components/v2/Header.tsx | 141 +++++++++++++++++- 12 files changed, 346 insertions(+), 152 deletions(-) diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/CodeWalkthrough.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/CodeWalkthrough.tsx index 22bbefdda8..27065f33fd 100644 --- a/website/src/app/(v2)/(marketing)/(index)/sections/CodeWalkthrough.tsx +++ b/website/src/app/(v2)/(marketing)/(index)/sections/CodeWalkthrough.tsx @@ -82,7 +82,7 @@ export const CodeWalkthrough = () => { }, []); return ( -
    +
    { transition={{ duration: 0.5 }} className="mb-16" > -

    How it works

    -

    +

    How it works

    +

    Rivet makes backend development feel like frontend development. Define your state, write your logic, and let the engine handle the rest.

    @@ -102,12 +102,14 @@ export const CodeWalkthrough = () => { {/* Sticky Code Block */}
    -
    +
    + {/* Top Shine Highlight */} +
    -
    -
    -
    +
    +
    +
    chat_room.ts
    @@ -177,10 +179,10 @@ export const CodeWalkthrough = () => { key={idx} data-index={idx} ref={(el) => (observerRefs.current[idx] = el)} - className={`transition-all duration-500 p-6 rounded-2xl border backdrop-blur-sm ${ + className={`transition-all duration-500 p-6 ${ idx === activeStep - ? "bg-white/[0.04] border-white/20 shadow-[0_0_30px_-10px_rgba(255,255,255,0.1)]" - : "bg-white/[0.02] border-white/5 opacity-50 hover:opacity-100 hover:bg-white/[0.05] hover:border-white/10" + ? "opacity-100" + : "opacity-50 blur-[1px] hover:opacity-100 hover:blur-0" }`} >
    diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/ConceptSection.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/ConceptSection.tsx index d85fe4f6c5..990cb920dc 100644 --- a/website/src/app/(v2)/(marketing)/(index)/sections/ConceptSection.tsx +++ b/website/src/app/(v2)/(marketing)/(index)/sections/ConceptSection.tsx @@ -22,9 +22,11 @@ const ArchitectureComparison = () => { whileInView={{ opacity: 1, x: 0 }} viewport={{ once: true }} transition={{ duration: 0.5 }} - className="p-6 rounded-2xl border border-white/5 bg-zinc-900/30 backdrop-blur-sm hover:bg-zinc-900/50 transition-colors duration-500" + className="p-6 rounded-2xl border border-white/5 bg-zinc-900/30 backdrop-blur-sm hover:bg-zinc-900/50 transition-colors duration-500 relative overflow-hidden" > -
    + {/* Top Shine Highlight */} +
    +
    @@ -102,8 +104,10 @@ const ArchitectureComparison = () => { transition={{ duration: 0.5 }} className="p-6 rounded-2xl border border-white/10 bg-zinc-900/30 backdrop-blur-md relative overflow-hidden shadow-[0_0_50px_-12px_rgba(16,185,129,0.1)] hover:shadow-[0_0_50px_-12px_rgba(16,185,129,0.2)] transition-shadow duration-500" > + {/* Top Shine Highlight (Green tinted for the 'hero' card) */} +
    -
    +
    @@ -176,7 +180,7 @@ const ArchitectureComparison = () => { }; export const ConceptSection = () => ( -
    +
    ( transition={{ duration: 0.5 }} className="flex-1" > -

    +

    Think in Actors,
    not just Functions.

    -
    -

    - What is an Actor? -
    - An Actor is a tiny, isolated server that holds its own data in memory. Unlike a stateless - function that forgets everything after it runs, an Actor remembers. -

    -

    - Why use them? -
    - When you need to manage state for a specific entity—like a Chat Room, a Game Match, or a User - Session—fetching that data from a database every 100ms is slow and expensive. Actors keep - that data in RAM, right next to your logic. -

    -
    +

    + What is an Actor? +
    + An Actor is a tiny, isolated server that holds its own data in memory. Unlike a stateless + function that forgets everything after it runs, an Actor remembers. +

    +

    + Why use them? +
    + When you need to manage state for a specific entity—like a Chat Room, a Game Match, or a User + Session—fetching that data from a database every 100ms is slow and expensive. Actors keep + that data in RAM, right next to your logic. +

    diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/FeaturesSection.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/FeaturesSection.tsx index 0bfb6fe329..f9fe2de65e 100644 --- a/website/src/app/(v2)/(marketing)/(index)/sections/FeaturesSection.tsx +++ b/website/src/app/(v2)/(marketing)/(index)/sections/FeaturesSection.tsx @@ -5,6 +5,10 @@ import { motion } from "framer-motion"; const FeatureCard = ({ title, description, code, graphic }) => (
    + {/* Top Shine Highlight */} +
    + {/* Bottom Fade Highlight */} +
    {/* Graphic Area */}
    {graphic} @@ -212,20 +216,20 @@ export const FeaturesSection = () => {
    {/* The Process Shell */} -
    +
    {/* The Persistent State (Core) */}
    - {/* Crash indicator overlay */} + {/* Crash indicator overlay (The 'X' or Alert) */}
    - {/* "Rebooting" Spinner ring */} -
    + {/* "Rebooting" Spinner ring appearing during crash */} +
    ), }, @@ -251,7 +255,7 @@ export const FeaturesSection = () => { whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: 0.1 }} - className="text-lg text-zinc-400" + className="text-lg text-zinc-400 leading-relaxed" > Rivet handles the hard parts of distributed systems: sharding, coordination, and persistence. You just write the logic. @@ -337,9 +341,9 @@ export const FeaturesSection = () => { } @keyframes shellRecover { - 0%, 45% { border-color: rgb(255, 69, 0); } - 50% { border-color: rgb(239 68 68); border-style: dashed; } - 60% { border-color: transparent; } + 0%, 47% { border-color: rgb(255, 69, 0); border-style: solid; } + 48%, 59% { border-color: rgb(239 68 68); border-style: dashed; } + 60% { border-color: transparent; border-style: solid; } 70%, 100% { border-color: rgb(255, 69, 0); border-style: solid; } } diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/HostingSection.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/HostingSection.tsx index c71d9f0dbc..ed62cc751b 100644 --- a/website/src/app/(v2)/(marketing)/(index)/sections/HostingSection.tsx +++ b/website/src/app/(v2)/(marketing)/(index)/sections/HostingSection.tsx @@ -4,7 +4,7 @@ import { Github, Cloud, Server, Check } from "lucide-react"; import { motion } from "framer-motion"; export const HostingSection = () => ( -
    +
    ( whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5 }} - className="text-3xl md:text-4xl font-medium text-white mb-4 tracking-tight" + className="text-3xl md:text-5xl font-medium text-white mb-6 tracking-tight" > Deploy your way. @@ -21,7 +21,7 @@ export const HostingSection = () => ( whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: 0.1 }} - className="text-zinc-400 max-w-2xl mx-auto" + className="text-lg text-zinc-400 max-w-2xl mx-auto leading-relaxed" > Start with the open-source binary on your laptop. Scale with Rivet Cloud. Go hybrid when you need total control over data residency. @@ -35,9 +35,11 @@ export const HostingSection = () => ( whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: 0.2 }} - className="p-8 rounded-2xl border border-white/10 bg-white/[0.02] hover:bg-white/[0.05] backdrop-blur-sm transition-all duration-300 flex flex-col hover:border-white/20 hover:shadow-[0_0_30px_-10px_rgba(255,255,255,0.1)]" + className="p-8 rounded-2xl border border-white/10 bg-white/[0.02] hover:bg-white/[0.05] backdrop-blur-sm transition-all duration-300 flex flex-col hover:border-white/20 hover:shadow-[0_0_30px_-10px_rgba(255,255,255,0.1)] relative overflow-hidden" > -
    + {/* Top Shine Highlight */} +
    +

    Open Source

    @@ -65,6 +67,8 @@ export const HostingSection = () => ( transition={{ duration: 0.5, delay: 0.3 }} className="p-8 rounded-2xl border border-white/10 bg-gradient-to-b from-[#FF4500]/10 to-transparent relative overflow-hidden group flex flex-col backdrop-blur-sm hover:border-[#FF4500]/30 transition-colors" > + {/* Top Shine Highlight (Orange) */} +
    @@ -88,9 +92,11 @@ export const HostingSection = () => ( whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: 0.4 }} - className="p-8 rounded-2xl border border-white/10 bg-white/[0.02] hover:bg-white/[0.05] backdrop-blur-sm transition-all duration-300 flex flex-col hover:border-white/20 hover:shadow-[0_0_30px_-10px_rgba(255,255,255,0.1)]" + className="p-8 rounded-2xl border border-white/10 bg-white/[0.02] hover:bg-white/[0.05] backdrop-blur-sm transition-all duration-300 flex flex-col hover:border-white/20 hover:shadow-[0_0_30px_-10px_rgba(255,255,255,0.1)] relative overflow-hidden" > -
    + {/* Top Shine Highlight */} +
    +

    Hybrid & On-Prem

    diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/IntegrationsSection.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/IntegrationsSection.tsx index 69a86ae799..725dc1bfbe 100644 --- a/website/src/app/(v2)/(marketing)/(index)/sections/IntegrationsSection.tsx +++ b/website/src/app/(v2)/(marketing)/(index)/sections/IntegrationsSection.tsx @@ -4,7 +4,7 @@ import { Box, LayoutGrid, Terminal, Wrench } from "lucide-react"; import { motion } from "framer-motion"; export const IntegrationsSection = () => ( -
    +
    ( transition={{ duration: 0.5 }} className="max-w-xl" > -

    +

    Stack Agnostic.

    -

    +

    Rivet actors are just standard TypeScript. They run in Docker, connect via WebSockets, and integrate effortlessly with your existing stack.

    @@ -25,21 +25,24 @@ export const IntegrationsSection = () => (
    - {/* Category 1: Infrastructure */} - -
    -
    + {/* Category 1: Infrastructure (Blue) */} +
    + {/* Top Shine Highlight - existing */} +
    + + {/* NEW: Top Left Reflection/Glow (Reduced opacity and soft fade) */} +
    + {/* NEW: Sharp Edge Highlight (Masked to Fade - Fixed Clipping) */} +
    + +
    + {/* Updated Icon Container */} +

    Infrastructure

    -
    +
    {["Docker", "Kubernetes", "Fly.io", "Railway", "AWS ECS"].map((tech) => ( ( ))}
    - +
    - {/* Category 2: Frameworks */} - -
    -
    + {/* Category 2: Frameworks (Purple) */} +
    + {/* Top Shine Highlight - existing */} +
    + + {/* NEW: Top Left Reflection/Glow (Reduced opacity and soft fade) */} +
    + {/* NEW: Sharp Edge Highlight (Masked to Fade - Fixed Clipping) */} +
    + +
    + {/* Updated Icon Container */} +

    Frameworks

    -
    +
    {["Next.js", "Remix", "React", "Vue", "Svelte", "Unity", "Godot"].map((tech) => ( ( ))}
    - +
    - {/* Category 3: Runtimes */} - -
    -
    + {/* Category 3: Runtimes (Yellow) */} +
    + {/* Top Shine Highlight - existing */} +
    + + {/* NEW: Top Left Reflection/Glow (Reduced opacity and soft fade) */} +
    + {/* NEW: Sharp Edge Highlight (Masked to Fade - Fixed Clipping) */} +
    + +
    + {/* Updated Icon Container */} +

    Runtimes

    -
    +
    {["Node.js", "Bun", "Deno", "Cloudflare Workers"].map((tech) => ( ( ))}
    - +
    - {/* Category 4: Tools */} - -
    -
    + {/* Category 4: Tools (Emerald) */} +
    + {/* Top Shine Highlight - existing */} +
    + + {/* NEW: Top Left Reflection/Glow (Reduced opacity and soft fade) */} +
    + {/* NEW: Sharp Edge Highlight (Masked to Fade - Fixed Clipping) */} +
    + +
    + {/* Updated Icon Container */} +

    Tools

    -
    +
    {["TypeScript", "ESLint", "Prettier", "Vite", "Turborepo"].map((tech) => ( ( ))}
    - +
    diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/ObservabilitySection.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/ObservabilitySection.tsx index 981174b332..c41a698000 100644 --- a/website/src/app/(v2)/(marketing)/(index)/sections/ObservabilitySection.tsx +++ b/website/src/app/(v2)/(marketing)/(index)/sections/ObservabilitySection.tsx @@ -48,7 +48,7 @@ export const ObservabilitySection = () => { whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: 0.1 }} - className="text-lg text-zinc-400 max-w-2xl mb-8" + className="text-lg text-zinc-400 max-w-2xl mb-8 leading-relaxed" > Powerful debugging and monitoring tools that work seamlessly from local development to production at scale. @@ -60,11 +60,11 @@ export const ObservabilitySection = () => { transition={{ duration: 0.5, delay: 0.2 }} className="flex flex-col sm:flex-row gap-4" > - - @@ -104,11 +104,13 @@ export const ObservabilitySection = () => { >
    + {/* Top Shine Highlight */} +
    {/* Window Bar */}
    -
    -
    -
    +
    +
    +
    {/* Content Area - Placeholder for Image */}
    diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/RedesignedCTA.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/RedesignedCTA.tsx index 82e0ce12fd..0e7a6b2ab6 100644 --- a/website/src/app/(v2)/(marketing)/(index)/sections/RedesignedCTA.tsx +++ b/website/src/app/(v2)/(marketing)/(index)/sections/RedesignedCTA.tsx @@ -16,7 +16,7 @@ export const RedesignedCTA = () => ( whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5 }} - className="text-4xl md:text-5xl font-medium text-white mb-6 tracking-tight" + className="text-4xl md:text-5xl font-medium text-white mb-8 tracking-tight" > Ready to build better backends? @@ -25,7 +25,7 @@ export const RedesignedCTA = () => ( whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: 0.1 }} - className="text-lg text-zinc-400 mb-10" + className="text-lg text-zinc-400 mb-10 leading-relaxed" > Join thousands of developers building the next generation of{" "}
    @@ -38,10 +38,10 @@ export const RedesignedCTA = () => ( transition={{ duration: 0.5, delay: 0.2 }} className="flex flex-col sm:flex-row items-center justify-center gap-4" > - - diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/RedesignedHero.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/RedesignedHero.tsx index 0d0f1740ce..d6aff217c1 100644 --- a/website/src/app/(v2)/(marketing)/(index)/sections/RedesignedHero.tsx +++ b/website/src/app/(v2)/(marketing)/(index)/sections/RedesignedHero.tsx @@ -1,33 +1,18 @@ "use client"; -import { Terminal, ArrowRight, Check, Copy } from "lucide-react"; -import { useState } from "react"; +import { Terminal, ArrowRight } from "lucide-react"; import { motion } from "framer-motion"; const CodeBlock = ({ code, fileName = "actor.ts" }) => { - const [copied, setCopied] = useState(false); - - const handleCopy = () => { - navigator.clipboard.writeText(code).catch(() => {}); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; - return (
    -
    -
    -
    +
    +
    +
    {fileName}
    -
    @@ -147,11 +132,11 @@ export const RedesignedHero = () => (
     						transition={{ duration: 0.5, delay: 0.3 }}
     						className="flex flex-col sm:flex-row items-center gap-4"
     					>
    -						
    -						
    @@ -193,3 +178,10 @@ export const counter = actor({
     		
    ); + + + + + + + diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/SolutionSection.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/SolutionSection.tsx index f391732492..9dacd9dec5 100644 --- a/website/src/app/(v2)/(marketing)/(index)/sections/SolutionSection.tsx +++ b/website/src/app/(v2)/(marketing)/(index)/sections/SolutionSection.tsx @@ -208,16 +208,16 @@ export function SolutionSection() { {/* Left Side: Sticky Code */}
    -
    +
    {/* Top Shine Highlight */}
    ai-agent.ts actor.ts
    -
    -
    -									
    +								
    +
    +									
     										// Define your actor
     										{"\n"}
     										export const aiAgent ={" "}
    diff --git a/website/src/app/(v2)/(marketing)/(index)/sections/SolutionsSection.tsx b/website/src/app/(v2)/(marketing)/(index)/sections/SolutionsSection.tsx
    index 1544fa21eb..f464b14460 100644
    --- a/website/src/app/(v2)/(marketing)/(index)/sections/SolutionsSection.tsx
    +++ b/website/src/app/(v2)/(marketing)/(index)/sections/SolutionsSection.tsx
    @@ -81,7 +81,7 @@ export const SolutionsSection = () => {
     						whileInView={{ opacity: 1, y: 0 }}
     						viewport={{ once: true }}
     						transition={{ duration: 0.5, delay: 0.1 }}
    -						className="text-lg text-zinc-400 max-w-2xl mx-auto"
    +						className="text-lg text-zinc-400 max-w-2xl mx-auto leading-relaxed"
     					>
     						If it needs to remember something, it belongs in an Actor.
     					
    @@ -95,16 +95,18 @@ export const SolutionsSection = () => {
     							whileInView={{ opacity: 1, y: 0 }}
     							viewport={{ once: true }}
     							transition={{ duration: 0.5, delay: idx * 0.05 }}
    -							className="p-6 rounded-xl border border-white/10 bg-white/[0.02] hover:bg-white/[0.05] backdrop-blur-sm transition-all duration-300 group flex flex-col justify-between hover:border-white/20 hover:shadow-[0_0_30px_-10px_rgba(255,255,255,0.1)]"
    +							className="p-6 rounded-xl border border-white/10 bg-white/[0.02] hover:bg-white/[0.05] backdrop-blur-sm transition-all duration-300 group flex flex-col justify-between hover:border-white/20 hover:shadow-[0_0_30px_-10px_rgba(255,255,255,0.1)] relative overflow-hidden"
     						>
    -							
    + {/* Top Shine Highlight */} +
    +
    {solution.icon}

    {solution.title}

    -

    {solution.description}

    +

    {solution.description}

    ))}
    diff --git a/website/src/app/(v2)/[section]/[[...page]]/page.tsx b/website/src/app/(v2)/[section]/[[...page]]/page.tsx index d11326adec..667169a76b 100644 --- a/website/src/app/(v2)/[section]/[[...page]]/page.tsx +++ b/website/src/app/(v2)/[section]/[[...page]]/page.tsx @@ -27,6 +27,9 @@ interface Param { page?: string[]; } +// Ensure Next.js knows this is a dynamic route +export const dynamicParams = false; + function createParamsForFile(section, file): Param { const step1 = file.replace("index.mdx", ""); const step2 = step1.replace(".mdx", ""); @@ -35,7 +38,7 @@ function createParamsForFile(section, file): Param { return { section, - page: step4, + page: step4.length > 0 ? step4 : undefined, }; } @@ -66,8 +69,11 @@ async function loadContent(path: string[]) { } export async function generateMetadata({ - params: { section, page }, + params, +}: { + params: { section: string; page?: string[] }; }): Promise { + const { section, page } = params; const path = buildPathComponents(section, page); const { component: { title, description }, @@ -85,7 +91,11 @@ export async function generateMetadata({ }; } -export default async function CatchAllCorePage({ params: { section, page } }) { +export default async function CatchAllCorePage({ + params: { section, page }, +}: { + params: { section: string; page?: string[] }; +}) { if (!VALID_SECTIONS.includes(section)) { return notFound(); } @@ -172,20 +182,51 @@ export default async function CatchAllCorePage({ params: { section, page } }) { } export async function generateStaticParams() { - const staticParams: Param[] = []; + const staticParams: Array<{ section: string; page?: string[] }> = []; + const seenParams = new Set(); for (const section of VALID_SECTIONS) { const dir = path.join(process.cwd(), "src", "content", section); - const dirs = await fs.readdir(dir, { recursive: true }); - const files = dirs.filter((file) => file.endsWith(".mdx")); - - const sectionParams = files.map((file) => { - const param = createParamsForFile(section, file); - return param; - }); + try { + // Always add base case first (section root with no page segments) + // For optional catch-all, omit page property when undefined + const baseKey = `${section}`; + if (!seenParams.has(baseKey)) { + seenParams.add(baseKey); + staticParams.push({ section }); + } - staticParams.push(...sectionParams); + // Read all MDX files recursively + 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); + + // For optional catch-all routes, omit page when undefined + const finalParam: { section: string; page?: string[] } = param.page === undefined + ? { section: param.section } + : { section: param.section, page: param.page }; + + // Create unique key for deduplication + const key = finalParam.page + ? `${finalParam.section}/${finalParam.page.join("/")}` + : finalParam.section; + + if (!seenParams.has(key)) { + seenParams.add(key); + staticParams.push(finalParam); + } + } + } catch (error) { + // If directory doesn't exist, still add base case + const baseKey = `${section}`; + if (!seenParams.has(baseKey)) { + seenParams.add(baseKey); + staticParams.push({ section }); + } + } } return staticParams; diff --git a/website/src/components/v2/Header.tsx b/website/src/components/v2/Header.tsx index 9d21b8b31a..ef461a06fc 100644 --- a/website/src/components/v2/Header.tsx +++ b/website/src/components/v2/Header.tsx @@ -10,6 +10,7 @@ import { type ReactNode, useEffect, useState } from "react"; import { usePathname, useRouter } from "next/navigation"; import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@rivet-gg/components"; import { faChevronDown } from "@rivet-gg/icons"; +import { Bot, Gamepad2, FileText, Workflow, Database } from "lucide-react"; import { GitHubDropdown } from "./GitHubDropdown"; import { HeaderSearch } from "./HeaderSearch"; import { LogoContextMenu } from "./LogoContextMenu"; @@ -47,6 +48,99 @@ function TextNavItem({ ); } +function SolutionsDropdown({ active }: { active?: boolean }) { + const [isOpen, setIsOpen] = useState(false); + + const solutions = [ + { + label: "Agents", + href: "/solutions/agents", + icon: Bot, + description: "Build durable AI assistants" + }, + { + label: "Game Servers", + href: "/solutions/game-servers", + icon: Gamepad2, + description: "Authoritative multiplayer servers" + }, + { + label: "Collaborative State", + href: "/solutions/collaborative-state", + icon: FileText, + description: "Real-time collaboration" + }, + { + label: "Workflows", + href: "/solutions/workflows", + icon: Workflow, + description: "Durable multi-step processes" + }, + { + label: "User-Session Store", + href: "/solutions/user-session-store", + icon: Database, + description: "Isolated user data stores" + }, + ]; + + return ( +
    setIsOpen(true)} + onMouseLeave={() => setIsOpen(false)} + > + + + + Solutions + + + + setIsOpen(true)} + onMouseLeave={() => setIsOpen(false)} + sideOffset={8} + alignOffset={0} + side="bottom" + > +
    + {solutions.map((solution) => { + const IconComponent = solution.icon; + return ( + +
    + +
    +
    +
    + {solution.label} +
    +
    + {solution.description} +
    +
    + + ); + })} +
    +
    +
    +
    + ); +} + interface HeaderProps { active?: "product" | "docs" | "blog" | "cloud" | "pricing" | "solutions"; subnav?: ReactNode; @@ -149,6 +243,7 @@ export function Header({ } breadcrumbs={
    + } breadcrumbs={
    + s.id === getCurrentSection()); return (
    {/* Main navigation links */} - {mainLinks.map(({ href, label }) => ( - - {label} - - ))} + {mainLinks.map(({ href, label, isDropdown }) => { + if (isDropdown) { + return ( +
    + + + + + + {solutions.map((solution) => ( + router.push(solution.href)} + > + {solution.label} + + ))} + + +
    + ); + } + return ( + + {label} + + ); + })} {/* Separator and docs content */} {isDocsPage && ( From f1a21fcb196247138c50aaf78ab183685a08942f Mon Sep 17 00:00:00 2001 From: Nicholas Kissel Date: Wed, 19 Nov 2025 23:07:07 -0800 Subject: [PATCH 04/13] Update lockfile to include @vbare/compiler dependency --- pnpm-lock.yaml | 111 ++++++++++++++++++++++++------------------------- 1 file changed, 55 insertions(+), 56 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24f828e45a..cb6c37199a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,6 +46,12 @@ importers: specifier: ^8.8.5 version: 8.8.5 + engine: + dependencies: + '@vbare/compiler': + specifier: ^0.0.3 + version: 0.0.3(@bare-ts/lib@0.4.0) + engine/docker/template: dependencies: '@types/js-yaml': @@ -1989,7 +1995,7 @@ importers: version: 3.13.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@uiw/codemirror-extensions-basic-setup': specifier: ^4.25.1 - version: 4.25.1(@codemirror/autocomplete@6.19.0)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.2) + version: 4.25.1(@codemirror/autocomplete@6.19.0)(@codemirror/commands@6.9.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.2) '@uiw/codemirror-theme-github': specifier: ^4.25.1 version: 4.25.1(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.2) @@ -2331,6 +2337,9 @@ importers: '@bare-ts/tools': specifier: ^0.13.0 version: 0.13.0(@bare-ts/lib@0.3.0) + '@biomejs/biome': + specifier: ^2.2.3 + version: 2.2.3 '@hono/node-server': specifier: ^1.18.2 version: 1.19.1(hono@4.9.8) @@ -2442,13 +2451,13 @@ importers: version: 6.0.1 '@mdx-js/loader': specifier: ^3.1.1 - version: 3.1.1(webpack@5.101.3) + version: 3.1.1(webpack@5.101.3(esbuild@0.25.9)) '@mdx-js/react': specifier: ^3.1.1 version: 3.1.1(@types/react@19.2.2)(react@19.1.1) '@next/mdx': specifier: ^15.5.2 - version: 15.5.2(@mdx-js/loader@3.1.1(webpack@5.101.3))(@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.1.1)) + version: 15.5.2(@mdx-js/loader@3.1.1(webpack@5.101.3(esbuild@0.25.9)))(@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.1.1)) '@next/third-parties': specifier: latest version: 16.0.1(next@15.5.2(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.93.2))(react@19.1.1) @@ -2638,7 +2647,7 @@ importers: version: 13.0.2(eslint@8.26.0)(typescript@5.9.2) file-loader: specifier: ^6.2.0 - version: 6.2.0(webpack@5.101.3) + version: 6.2.0(webpack@5.101.3(esbuild@0.25.9)) prettier: specifier: ^2.8.8 version: 2.8.8 @@ -3250,6 +3259,13 @@ packages: peerDependencies: '@bare-ts/lib': '>=0.3.0 <=0.4.0' + '@bare-ts/tools@0.16.1': + resolution: {integrity: sha512-eKXTnVqzuKDxr1ozKsFSZfM1wcN4g/iMRnG9GB2fA8oyUcHxwokJC50CANfuSLe6rLnjhZ8Ave1Y2TnZqUqGcQ==} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + '@bare-ts/lib': '>=0.3.0 <=0.4.0' + '@base-org/account@2.0.1': resolution: {integrity: sha512-tySVNx+vd6XEynZL0uvB10uKiwnAfThr8AbKTwILVG86mPbLAhEOInQIk+uDnvpTvfdUhC1Bi5T/46JvFoLZQQ==} @@ -7021,6 +7037,11 @@ packages: peerDependencies: '@urql/core': ^5.0.0 + '@vbare/compiler@0.0.3': + resolution: {integrity: sha512-Dhz0iwYjIhyGAPsNpiqDmDqgwLXfEonjFJLVQ0m/s4Tt9CsTjY0WV3KiQtJi5BdPt9481HR+0uwExH36FuuR2A==} + engines: {node: '>=18.0.0'} + hasBin: true + '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -14436,6 +14457,10 @@ snapshots: '@bare-ts/lib': 0.4.0 commander: 11.1.0 + '@bare-ts/tools@0.16.1(@bare-ts/lib@0.4.0)': + dependencies: + '@bare-ts/lib': 0.4.0 + '@base-org/account@2.0.1(@types/react@19.2.2)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.1.1))(zod@3.25.76)': dependencies: '@noble/hashes': 1.4.0 @@ -16201,12 +16226,12 @@ snapshots: '@marijn/find-cluster-break@1.0.2': {} - '@mdx-js/loader@3.1.1(webpack@5.101.3)': + '@mdx-js/loader@3.1.1(webpack@5.101.3(esbuild@0.25.9))': dependencies: '@mdx-js/mdx': 3.1.1 source-map: 0.7.6 optionalDependencies: - webpack: 5.101.3 + webpack: 5.101.3(esbuild@0.25.9) transitivePeerDependencies: - supports-color @@ -16384,11 +16409,11 @@ snapshots: dependencies: glob: 7.1.7 - '@next/mdx@15.5.2(@mdx-js/loader@3.1.1(webpack@5.101.3))(@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.1.1))': + '@next/mdx@15.5.2(@mdx-js/loader@3.1.1(webpack@5.101.3(esbuild@0.25.9)))(@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.1.1))': dependencies: source-map: 0.7.6 optionalDependencies: - '@mdx-js/loader': 3.1.1(webpack@5.101.3) + '@mdx-js/loader': 3.1.1(webpack@5.101.3(esbuild@0.25.9)) '@mdx-js/react': 3.1.1(@types/react@19.2.2)(react@19.1.1) '@next/swc-darwin-arm64@15.4.5': @@ -18615,6 +18640,16 @@ snapshots: '@codemirror/state': 6.5.2 '@codemirror/view': 6.38.2 + '@uiw/codemirror-extensions-basic-setup@4.25.1(@codemirror/autocomplete@6.19.0)(@codemirror/commands@6.9.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.2)': + dependencies: + '@codemirror/autocomplete': 6.19.0 + '@codemirror/commands': 6.9.0 + '@codemirror/language': 6.11.3 + '@codemirror/lint': 6.9.0 + '@codemirror/search': 6.5.11 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.2 + '@uiw/codemirror-theme-github@4.25.1(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.2)': dependencies: '@uiw/codemirror-themes': 4.25.1(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.2) @@ -18677,6 +18712,13 @@ snapshots: '@urql/core': 5.2.0 wonka: 6.3.5 + '@vbare/compiler@0.0.3(@bare-ts/lib@0.4.0)': + dependencies: + '@bare-ts/tools': 0.16.1(@bare-ts/lib@0.4.0) + commander: 11.1.0 + transitivePeerDependencies: + - '@bare-ts/lib' + '@vitejs/plugin-react@4.7.0(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0))': dependencies: '@babel/core': 7.28.4 @@ -20670,7 +20712,7 @@ snapshots: eslint: 8.26.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 2.7.1(eslint-plugin-import@2.32.0(eslint@8.26.0))(eslint@8.26.0) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.26.0)(typescript@5.9.2))(eslint-import-resolver-typescript@2.7.1)(eslint@8.26.0) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.26.0)(typescript@5.9.2))(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.32.0(eslint@8.26.0))(eslint@8.26.0))(eslint@8.26.0) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.26.0) eslint-plugin-react: 7.37.5(eslint@8.26.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.26.0) @@ -20692,7 +20734,7 @@ snapshots: dependencies: debug: 4.4.1 eslint: 8.26.0 - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.26.0)(typescript@5.9.2))(eslint-import-resolver-typescript@2.7.1)(eslint@8.26.0) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.26.0)(typescript@5.9.2))(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.32.0(eslint@8.26.0))(eslint@8.26.0))(eslint@8.26.0) glob: 7.2.3 is-glob: 4.0.3 resolve: 1.22.10 @@ -20711,7 +20753,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.26.0)(typescript@5.9.2))(eslint-import-resolver-typescript@2.7.1)(eslint@8.26.0): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.26.0)(typescript@5.9.2))(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.32.0(eslint@8.26.0))(eslint@8.26.0))(eslint@8.26.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -21210,11 +21252,11 @@ snapshots: dependencies: flat-cache: 3.2.0 - file-loader@6.2.0(webpack@5.101.3): + file-loader@6.2.0(webpack@5.101.3(esbuild@0.25.9)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.101.3 + webpack: 5.101.3(esbuild@0.25.9) file-saver@2.0.5: {} @@ -25745,16 +25787,6 @@ snapshots: webpack: 5.101.3(esbuild@0.25.9) optionalDependencies: esbuild: 0.25.9 - optional: true - - terser-webpack-plugin@5.3.14(webpack@5.101.3): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - jest-worker: 27.5.1 - schema-utils: 4.3.3 - serialize-javascript: 6.0.2 - terser: 5.44.0 - webpack: 5.101.3 terser@5.44.0: dependencies: @@ -26878,38 +26910,6 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.101.3: - dependencies: - '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.15.0 - acorn-import-phases: 1.0.4(acorn@8.15.0) - browserslist: 4.26.3 - chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.3 - es-module-lexer: 1.7.0 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.1 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 4.3.3 - tapable: 2.3.0 - terser-webpack-plugin: 5.3.14(webpack@5.101.3) - watchpack: 2.4.4 - webpack-sources: 3.3.3 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - webpack@5.101.3(esbuild@0.25.9): dependencies: '@types/eslint-scope': 3.7.7 @@ -26941,7 +26941,6 @@ snapshots: - '@swc/core' - esbuild - uglify-js - optional: true whatwg-fetch@3.6.20: {} From 066f34983f8fcc8d3e615366dfcee60e42347e18 Mon Sep 17 00:00:00 2001 From: Nicholas Kissel Date: Wed, 19 Nov 2025 23:30:15 -0800 Subject: [PATCH 05/13] Fix: Correct icon imports (faNextJs->faNextjs, faGitHub->faGithub) and ensure generateStaticParams is async --- .../page.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/src/posts/2025-10-20-how-we-built-websocket-servers-for-vercel-functions/page.mdx b/website/src/posts/2025-10-20-how-we-built-websocket-servers-for-vercel-functions/page.mdx index f5625bc1bd..5523fb3b10 100644 --- a/website/src/posts/2025-10-20-how-we-built-websocket-servers-for-vercel-functions/page.mdx +++ b/website/src/posts/2025-10-20-how-we-built-websocket-servers-for-vercel-functions/page.mdx @@ -1,8 +1,8 @@ import { - faGitHub, + faGithub, faDiscord, faCloud, - faNextJs + faNextjs } from "@rivet-gg/icons"; import imgNoTunneling from "./no-tunneling.png"; import imgTunneling1 from "./tunneling-1.png"; @@ -538,7 +538,7 @@ At Rivet, we're able to help provide WebSockets on Vercel because of two core co Date: Wed, 19 Nov 2025 23:34:29 -0800 Subject: [PATCH 06/13] Fix: Move generateStaticParams before default export for Next.js 15 compatibility --- .../app/(v2)/[section]/[[...page]]/page.tsx | 102 +++++++++--------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/website/src/app/(v2)/[section]/[[...page]]/page.tsx b/website/src/app/(v2)/[section]/[[...page]]/page.tsx index 667169a76b..09cc246a84 100644 --- a/website/src/app/(v2)/[section]/[[...page]]/page.tsx +++ b/website/src/app/(v2)/[section]/[[...page]]/page.tsx @@ -91,6 +91,57 @@ export async function generateMetadata({ }; } +export async function generateStaticParams() { + const staticParams: Array<{ section: string; page?: string[] }> = []; + const seenParams = new Set(); + + for (const section of VALID_SECTIONS) { + const dir = path.join(process.cwd(), "src", "content", section); + + try { + // Always add base case first (section root with no page segments) + // For optional catch-all, omit page property when undefined + const baseKey = `${section}`; + if (!seenParams.has(baseKey)) { + seenParams.add(baseKey); + staticParams.push({ section }); + } + + // Read all MDX files recursively + 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); + + // For optional catch-all routes, omit page when undefined + const finalParam: { section: string; page?: string[] } = param.page === undefined + ? { section: param.section } + : { section: param.section, page: param.page }; + + // Create unique key for deduplication + const key = finalParam.page + ? `${finalParam.section}/${finalParam.page.join("/")}` + : finalParam.section; + + if (!seenParams.has(key)) { + seenParams.add(key); + staticParams.push(finalParam); + } + } + } catch (error) { + // If directory doesn't exist, still add base case + const baseKey = `${section}`; + if (!seenParams.has(baseKey)) { + seenParams.add(baseKey); + staticParams.push({ section }); + } + } + } + + return staticParams; +} + export default async function CatchAllCorePage({ params: { section, page }, }: { @@ -180,54 +231,3 @@ export default async function CatchAllCorePage({ ); } - -export async function generateStaticParams() { - const staticParams: Array<{ section: string; page?: string[] }> = []; - const seenParams = new Set(); - - for (const section of VALID_SECTIONS) { - const dir = path.join(process.cwd(), "src", "content", section); - - try { - // Always add base case first (section root with no page segments) - // For optional catch-all, omit page property when undefined - const baseKey = `${section}`; - if (!seenParams.has(baseKey)) { - seenParams.add(baseKey); - staticParams.push({ section }); - } - - // Read all MDX files recursively - 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); - - // For optional catch-all routes, omit page when undefined - const finalParam: { section: string; page?: string[] } = param.page === undefined - ? { section: param.section } - : { section: param.section, page: param.page }; - - // Create unique key for deduplication - const key = finalParam.page - ? `${finalParam.section}/${finalParam.page.join("/")}` - : finalParam.section; - - if (!seenParams.has(key)) { - seenParams.add(key); - staticParams.push(finalParam); - } - } - } catch (error) { - // If directory doesn't exist, still add base case - const baseKey = `${section}`; - if (!seenParams.has(baseKey)) { - seenParams.add(baseKey); - staticParams.push({ section }); - } - } - } - - return staticParams; -} From 51b5232f93deb01c3f8ea71632a3ae32d83aea5c Mon Sep 17 00:00:00 2001 From: Nicholas Kissel Date: Wed, 19 Nov 2025 23:47:08 -0800 Subject: [PATCH 07/13] Fix: Make generateStaticParams synchronous to match Next.js 15 requirements --- website/src/app/(v2)/[section]/[[...page]]/page.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/website/src/app/(v2)/[section]/[[...page]]/page.tsx b/website/src/app/(v2)/[section]/[[...page]]/page.tsx index 09cc246a84..79fb9b8dbf 100644 --- a/website/src/app/(v2)/[section]/[[...page]]/page.tsx +++ b/website/src/app/(v2)/[section]/[[...page]]/page.tsx @@ -91,7 +91,7 @@ export async function generateMetadata({ }; } -export async function generateStaticParams() { +async function generateStaticParamsImpl() { const staticParams: Array<{ section: string; page?: string[] }> = []; const seenParams = new Set(); @@ -142,6 +142,10 @@ export async function generateStaticParams() { return staticParams; } +export function generateStaticParams() { + return generateStaticParamsImpl(); +} + export default async function CatchAllCorePage({ params: { section, page }, }: { From b0561c21b808f3c8f1a2ca72e870e5935686c724 Mon Sep 17 00:00:00 2001 From: Nicholas Kissel Date: Wed, 19 Nov 2025 23:55:27 -0800 Subject: [PATCH 08/13] Fix: Make generateStaticParams directly async with explicit return type --- website/src/app/(v2)/[section]/[[...page]]/page.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/website/src/app/(v2)/[section]/[[...page]]/page.tsx b/website/src/app/(v2)/[section]/[[...page]]/page.tsx index 79fb9b8dbf..fd8d2735f9 100644 --- a/website/src/app/(v2)/[section]/[[...page]]/page.tsx +++ b/website/src/app/(v2)/[section]/[[...page]]/page.tsx @@ -91,7 +91,9 @@ export async function generateMetadata({ }; } -async function generateStaticParamsImpl() { +export async function generateStaticParams(): Promise< + Array<{ section: string; page?: string[] }> +> { const staticParams: Array<{ section: string; page?: string[] }> = []; const seenParams = new Set(); @@ -142,10 +144,6 @@ async function generateStaticParamsImpl() { return staticParams; } -export function generateStaticParams() { - return generateStaticParamsImpl(); -} - export default async function CatchAllCorePage({ params: { section, page }, }: { From 49e23fecf2dc7e86752ce48cdaa24b8a93d82543 Mon Sep 17 00:00:00 2001 From: Nicholas Kissel Date: Thu, 20 Nov 2025 00:00:29 -0800 Subject: [PATCH 09/13] Fix: Export generateStaticParams as const arrow for detection --- website/src/app/(v2)/[section]/[[...page]]/page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/src/app/(v2)/[section]/[[...page]]/page.tsx b/website/src/app/(v2)/[section]/[[...page]]/page.tsx index fd8d2735f9..7cffebd9b9 100644 --- a/website/src/app/(v2)/[section]/[[...page]]/page.tsx +++ b/website/src/app/(v2)/[section]/[[...page]]/page.tsx @@ -91,9 +91,9 @@ export async function generateMetadata({ }; } -export async function generateStaticParams(): Promise< +export const generateStaticParams = async (): Promise< Array<{ section: string; page?: string[] }> -> { +> => { const staticParams: Array<{ section: string; page?: string[] }> = []; const seenParams = new Set(); @@ -142,7 +142,7 @@ export async function generateStaticParams(): Promise< } return staticParams; -} +}; export default async function CatchAllCorePage({ params: { section, page }, From fc897b7b1300f6b5a82f052cd3dd5b1a1dc1a11f Mon Sep 17 00:00:00 2001 From: Nicholas Kissel Date: Thu, 20 Nov 2025 10:37:13 -0800 Subject: [PATCH 10/13] Fix: Use function declaration for generateStaticParams to ensure Next.js detection --- website/src/app/(v2)/[section]/[[...page]]/page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/src/app/(v2)/[section]/[[...page]]/page.tsx b/website/src/app/(v2)/[section]/[[...page]]/page.tsx index 7cffebd9b9..fd8d2735f9 100644 --- a/website/src/app/(v2)/[section]/[[...page]]/page.tsx +++ b/website/src/app/(v2)/[section]/[[...page]]/page.tsx @@ -91,9 +91,9 @@ export async function generateMetadata({ }; } -export const generateStaticParams = async (): Promise< +export async function generateStaticParams(): Promise< Array<{ section: string; page?: string[] }> -> => { +> { const staticParams: Array<{ section: string; page?: string[] }> = []; const seenParams = new Set(); @@ -142,7 +142,7 @@ export const generateStaticParams = async (): Promise< } return staticParams; -}; +} export default async function CatchAllCorePage({ params: { section, page }, From 7e1be81e5fc250096f0299680d6ce46aea55596f Mon Sep 17 00:00:00 2001 From: Nicholas Kissel Date: Thu, 20 Nov 2025 10:37:43 -0800 Subject: [PATCH 11/13] Fix: Simplify generateStaticParams return type annotation --- website/src/app/(v2)/[section]/[[...page]]/page.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/website/src/app/(v2)/[section]/[[...page]]/page.tsx b/website/src/app/(v2)/[section]/[[...page]]/page.tsx index fd8d2735f9..09cc246a84 100644 --- a/website/src/app/(v2)/[section]/[[...page]]/page.tsx +++ b/website/src/app/(v2)/[section]/[[...page]]/page.tsx @@ -91,9 +91,7 @@ export async function generateMetadata({ }; } -export async function generateStaticParams(): Promise< - Array<{ section: string; page?: string[] }> -> { +export async function generateStaticParams() { const staticParams: Array<{ section: string; page?: string[] }> = []; const seenParams = new Set(); From 001ab12997884b6f07e039c5ed55499c8b997dc1 Mon Sep 17 00:00:00 2001 From: Nicholas Kissel Date: Thu, 20 Nov 2025 11:16:15 -0800 Subject: [PATCH 12/13] Fix: Use const arrow function format for generateStaticParams to match other routes --- website/src/app/(v2)/[section]/[[...page]]/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/app/(v2)/[section]/[[...page]]/page.tsx b/website/src/app/(v2)/[section]/[[...page]]/page.tsx index 09cc246a84..726e6e43a1 100644 --- a/website/src/app/(v2)/[section]/[[...page]]/page.tsx +++ b/website/src/app/(v2)/[section]/[[...page]]/page.tsx @@ -91,7 +91,7 @@ export async function generateMetadata({ }; } -export async function generateStaticParams() { +export const generateStaticParams = async () => { const staticParams: Array<{ section: string; page?: string[] }> = []; const seenParams = new Set(); @@ -140,7 +140,7 @@ export async function generateStaticParams() { } return staticParams; -} +}; export default async function CatchAllCorePage({ params: { section, page }, From 30377cb3e9fac8e9603ab41c2694f158c5f3c35b Mon Sep 17 00:00:00 2001 From: Nicholas Kissel Date: Thu, 20 Nov 2025 13:34:14 -0800 Subject: [PATCH 13/13] Fix: Match generateStaticParams from working branch - simpler function after default export --- .../app/(v2)/[section]/[[...page]]/page.tsx | 73 ++++++------------- 1 file changed, 21 insertions(+), 52 deletions(-) diff --git a/website/src/app/(v2)/[section]/[[...page]]/page.tsx b/website/src/app/(v2)/[section]/[[...page]]/page.tsx index 726e6e43a1..d6505d2f84 100644 --- a/website/src/app/(v2)/[section]/[[...page]]/page.tsx +++ b/website/src/app/(v2)/[section]/[[...page]]/page.tsx @@ -38,7 +38,7 @@ function createParamsForFile(section, file): Param { return { section, - page: step4.length > 0 ? step4 : undefined, + page: step4, }; } @@ -91,57 +91,6 @@ export async function generateMetadata({ }; } -export const generateStaticParams = async () => { - const staticParams: Array<{ section: string; page?: string[] }> = []; - const seenParams = new Set(); - - for (const section of VALID_SECTIONS) { - const dir = path.join(process.cwd(), "src", "content", section); - - try { - // Always add base case first (section root with no page segments) - // For optional catch-all, omit page property when undefined - const baseKey = `${section}`; - if (!seenParams.has(baseKey)) { - seenParams.add(baseKey); - staticParams.push({ section }); - } - - // Read all MDX files recursively - 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); - - // For optional catch-all routes, omit page when undefined - const finalParam: { section: string; page?: string[] } = param.page === undefined - ? { section: param.section } - : { section: param.section, page: param.page }; - - // Create unique key for deduplication - const key = finalParam.page - ? `${finalParam.section}/${finalParam.page.join("/")}` - : finalParam.section; - - if (!seenParams.has(key)) { - seenParams.add(key); - staticParams.push(finalParam); - } - } - } catch (error) { - // If directory doesn't exist, still add base case - const baseKey = `${section}`; - if (!seenParams.has(baseKey)) { - seenParams.add(baseKey); - staticParams.push({ section }); - } - } - } - - return staticParams; -}; - export default async function CatchAllCorePage({ params: { section, page }, }: { @@ -231,3 +180,23 @@ export default async function CatchAllCorePage({ ); } + +export async function generateStaticParams() { + const staticParams: Param[] = []; + + for (const section of VALID_SECTIONS) { + const dir = path.join(process.cwd(), "src", "content", section); + + const dirs = await fs.readdir(dir, { recursive: true }); + const files = dirs.filter((file) => file.endsWith(".mdx")); + + const sectionParams = files.map((file) => { + const param = createParamsForFile(section, file); + return param; + }); + + staticParams.push(...sectionParams); + } + + return staticParams; +}