From d7f4e0d906241782ec69fcd430f7afead3c9d723 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Sun, 30 Nov 2025 23:09:29 -0800 Subject: [PATCH] feat(site): add templates --- pnpm-lock.yaml | 9 + website/package.json | 3 +- .../templates/TemplatesPageClient.tsx | 155 +++++++ .../templates/[slug]/TemplateDetailClient.tsx | 155 +++++++ .../(marketing)/templates/[slug]/page.tsx | 67 +++ .../templates/components/TemplateCard.tsx | 62 +++ .../templates/components/TemplatesSidebar.tsx | 81 ++++ .../app/(v2)/(marketing)/templates/page.tsx | 15 + website/src/components/v2/Header.tsx | 15 + website/src/data/templates/shared.ts | 393 ++++++++++++++++++ 10 files changed, 954 insertions(+), 1 deletion(-) create mode 100644 website/src/app/(v2)/(marketing)/templates/TemplatesPageClient.tsx create mode 100644 website/src/app/(v2)/(marketing)/templates/[slug]/TemplateDetailClient.tsx create mode 100644 website/src/app/(v2)/(marketing)/templates/[slug]/page.tsx create mode 100644 website/src/app/(v2)/(marketing)/templates/components/TemplateCard.tsx create mode 100644 website/src/app/(v2)/(marketing)/templates/components/TemplatesSidebar.tsx create mode 100644 website/src/app/(v2)/(marketing)/templates/page.tsx create mode 100644 website/src/data/templates/shared.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e840e939a..134fe943ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2509,6 +2509,9 @@ importers: framer-motion: specifier: ^12.23.24 version: 12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + fuse.js: + specifier: ^7.1.0 + version: 7.1.0 jszip: specifier: ^3.10.1 version: 3.10.1 @@ -9812,6 +9815,10 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + fuse.js@7.1.0: + resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==} + engines: {node: '>=10'} + generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -22671,6 +22678,8 @@ snapshots: functions-have-names@1.2.3: {} + fuse.js@7.1.0: {} + generator-function@2.0.1: {} gensync@1.0.0-beta.2: {} diff --git a/website/package.json b/website/package.json index 48f10fe087..cb8f84bb31 100644 --- a/website/package.json +++ b/website/package.json @@ -55,6 +55,7 @@ "feed": "^5.1.0", "focus-visible": "^5.2.1", "framer-motion": "^12.23.24", + "fuse.js": "^7.1.0", "jszip": "^3.10.1", "mdast-util-to-string": "^4.0.0", "mdx-annotations": "^0.1.4", @@ -65,8 +66,8 @@ "postcss-focus-visible": "^10.0.1", "posthog-js": "^1.297.0", "react": "^19.2.0", - "react-dom": "^19.2.0", "react-chartjs-2": "^5.3.1", + "react-dom": "^19.2.0", "react-github-btn": "^1.4.0", "react-highlight-words": "^0.21.0", "react-markdown": "^10.1.0", diff --git a/website/src/app/(v2)/(marketing)/templates/TemplatesPageClient.tsx b/website/src/app/(v2)/(marketing)/templates/TemplatesPageClient.tsx new file mode 100644 index 0000000000..22e884cde9 --- /dev/null +++ b/website/src/app/(v2)/(marketing)/templates/TemplatesPageClient.tsx @@ -0,0 +1,155 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { templates, type Technology, type Tag } from "@/data/templates/shared"; +import { TemplateCard } from "./components/TemplateCard"; +import { TemplatesSidebar } from "./components/TemplatesSidebar"; +import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import Fuse from "fuse.js"; + +export default function TemplatesPageClient() { + const [searchQuery, setSearchQuery] = useState(""); + const [selectedTags, setSelectedTags] = useState([]); + const [selectedTechnologies, setSelectedTechnologies] = useState([]); + + // Get unique tags and technologies from all templates + const allTags = useMemo(() => { + const tagsSet = new Set(); + templates.forEach((template) => { + template.tags.forEach((tag) => tagsSet.add(tag)); + }); + return Array.from(tagsSet).sort(); + }, []); + + const allTechnologies = useMemo(() => { + const techSet = new Set(); + templates.forEach((template) => { + template.technologies.forEach((tech) => techSet.add(tech)); + }); + return Array.from(techSet).sort(); + }, []); + + // Configure Fuse.js for fuzzy searching + const fuse = useMemo(() => { + return new Fuse(templates, { + keys: [ + { name: "displayName", weight: 2 }, + { name: "description", weight: 1.5 }, + { name: "tags", weight: 1 }, + { name: "technologies", weight: 1 }, + ], + threshold: 0.4, + includeScore: true, + }); + }, []); + + // Filter templates based on search and selections + const filteredTemplates = useMemo(() => { + let results = templates; + + // Apply fuzzy search if there's a query + if (searchQuery.trim() !== "") { + const fuseResults = fuse.search(searchQuery); + results = fuseResults.map((result) => result.item); + } + + // Apply tag and technology filters + results = results.filter((template) => { + // Tags filter + const matchesTags = + selectedTags.length === 0 || + selectedTags.some((tag) => template.tags.includes(tag)); + + // Technologies filter + const matchesTechnologies = + selectedTechnologies.length === 0 || + selectedTechnologies.some((tech) => template.technologies.includes(tech)); + + return matchesTags && matchesTechnologies; + }); + + return results; + }, [searchQuery, selectedTags, selectedTechnologies, fuse]); + + const handleTagToggle = (tag: Tag) => { + setSelectedTags((prev) => + prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag], + ); + }; + + const handleTechnologyToggle = (tech: Technology) => { + setSelectedTechnologies((prev) => + prev.includes(tech) ? prev.filter((t) => t !== tech) : [...prev, tech], + ); + }; + + return ( +
+
+
+
+

+ Templates +

+

+ Explore RivetKit templates and examples to quickly start building + with Rivet Actors +

+
+
+
+
+ setSearchQuery(e.target.value)} + className="block w-full rounded-lg border border-white/20 bg-white/5 pl-10 pr-3 py-3 text-white placeholder:text-zinc-500 focus:border-[#FF4500] focus:outline-none focus:ring-1 focus:ring-[#FF4500] text-base" + placeholder="Search templates..." + /> +
+
+
+
+
+ +
+
+ {/* Sidebar */} + + + {/* Main Grid */} +
+ {filteredTemplates.length === 0 ? ( +
+ No templates found matching your filters +
+ ) : ( +
+ {filteredTemplates.map((template) => ( + + ))} +
+ )} +
+
+
+
+ ); +} diff --git a/website/src/app/(v2)/(marketing)/templates/[slug]/TemplateDetailClient.tsx b/website/src/app/(v2)/(marketing)/templates/[slug]/TemplateDetailClient.tsx new file mode 100644 index 0000000000..56ce81beb0 --- /dev/null +++ b/website/src/app/(v2)/(marketing)/templates/[slug]/TemplateDetailClient.tsx @@ -0,0 +1,155 @@ +"use client"; + +import type { Template } from "@/data/templates/shared"; +import { templates } from "@/data/templates/shared"; +import { Markdown } from "@/components/Markdown"; +import { TemplateCard } from "../components/TemplateCard"; +import { Icon, faGithub } from "@rivet-gg/icons"; +import Link from "next/link"; + +interface TemplateDetailClientProps { + template: Template; + readmeContent: string; +} + +export default function TemplateDetailClient({ + template, + readmeContent, +}: TemplateDetailClientProps) { + // Find related templates based on shared tags + const relatedTemplates = templates + .filter((t) => { + // Exclude the current template + if (t.name === template.name) return false; + + // Find templates with at least one shared tag + return template.tags.some((tag) => t.tags.includes(tag)); + }) + .slice(0, 3); + + // If no related templates with shared tags, just show any 3 templates + const displayedRelated = + relatedTemplates.length > 0 + ? relatedTemplates + : templates.filter((t) => t.name !== template.name).slice(0, 3); + + const githubUrl = `https://github.com/rivet-dev/rivetkit/tree/main/examples/${template.name}`; + + return ( +
+ {/* Header with Image */} +
+
+ {/* Placeholder Image */} +
+ + + +
+ + {/* Title and Description */} +
+

+ {template.displayName} +

+

+ {template.description} +

+
+
+
+ + {/* Content Section */} +
+
+ {/* Left Column - README Content */} +
+
+ {readmeContent} +
+
+ + {/* Right Column - Sidebar */} + +
+ + {/* Related Templates Section */} +
+

+ Related Templates +

+
+ {displayedRelated.map((relatedTemplate) => ( + + ))} +
+
+
+
+ ); +} diff --git a/website/src/app/(v2)/(marketing)/templates/[slug]/page.tsx b/website/src/app/(v2)/(marketing)/templates/[slug]/page.tsx new file mode 100644 index 0000000000..7121047c1b --- /dev/null +++ b/website/src/app/(v2)/(marketing)/templates/[slug]/page.tsx @@ -0,0 +1,67 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { templates } from "@/data/templates/shared"; +import fs from "node:fs/promises"; +import path from "node:path"; +import TemplateDetailClient from "./TemplateDetailClient"; + +interface Props { + params: Promise<{ slug: string }>; +} + +export async function generateStaticParams() { + return templates.map((template) => ({ + slug: template.name, + })); +} + +export async function generateMetadata({ params }: Props): Promise { + const { slug } = await params; + const template = templates.find((t) => t.name === slug); + + if (!template) { + return { + title: "Template Not Found - Rivet", + }; + } + + return { + title: `${template.displayName} - Rivet Templates`, + description: template.description, + alternates: { + canonical: `https://www.rivet.dev/templates/${slug}/`, + }, + }; +} + +async function getReadmeContent(templateName: string): Promise { + try { + const readmePath = path.join( + process.cwd(), + "..", + "examples", + templateName, + "README.md", + ); + const content = await fs.readFile(readmePath, "utf-8"); + return content; + } catch (error) { + console.error(`Failed to read README for ${templateName}:`, error); + return "# README not found\n\nThe README for this template could not be loaded."; + } +} + +export default async function Page({ params }: Props) { + const { slug } = await params; + const template = templates.find((t) => t.name === slug); + + if (!template) { + notFound(); + } + + const readmeContent = await getReadmeContent(template.name); + + return ( + + ); +} diff --git a/website/src/app/(v2)/(marketing)/templates/components/TemplateCard.tsx b/website/src/app/(v2)/(marketing)/templates/components/TemplateCard.tsx new file mode 100644 index 0000000000..928bb0d96f --- /dev/null +++ b/website/src/app/(v2)/(marketing)/templates/components/TemplateCard.tsx @@ -0,0 +1,62 @@ +import type { Template } from "@/data/templates/shared"; +import Link from "next/link"; +import clsx from "clsx"; + +interface TemplateCardProps { + template: Template; +} + +export function TemplateCard({ template }: TemplateCardProps) { + return ( + +
+ {/* Placeholder Image */} +
+ + + +
+ + {/* Content */} +
+ {/* Title */} +

+ {template.displayName} +

+ + {/* Description */} +

+ {template.description} +

+ + {/* Technologies */} +
+ {template.technologies.map((tech) => ( + + {tech} + + ))} +
+
+
+ + ); +} diff --git a/website/src/app/(v2)/(marketing)/templates/components/TemplatesSidebar.tsx b/website/src/app/(v2)/(marketing)/templates/components/TemplatesSidebar.tsx new file mode 100644 index 0000000000..c1cf54d17f --- /dev/null +++ b/website/src/app/(v2)/(marketing)/templates/components/TemplatesSidebar.tsx @@ -0,0 +1,81 @@ +import type { Technology, Tag } from "@/data/templates/shared"; + +interface TemplatesSidebarProps { + allTags: Tag[]; + selectedTags: Tag[]; + onTagToggle: (tag: Tag) => void; + allTechnologies: Technology[]; + selectedTechnologies: Technology[]; + onTechnologyToggle: (tech: Technology) => void; +} + +export function TemplatesSidebar({ + allTags, + selectedTags, + onTagToggle, + allTechnologies, + selectedTechnologies, + onTechnologyToggle, +}: TemplatesSidebarProps) { + return ( + + ); +} diff --git a/website/src/app/(v2)/(marketing)/templates/page.tsx b/website/src/app/(v2)/(marketing)/templates/page.tsx new file mode 100644 index 0000000000..ca567ce54f --- /dev/null +++ b/website/src/app/(v2)/(marketing)/templates/page.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from "next"; +import TemplatesPageClient from "./TemplatesPageClient"; + +export const metadata: Metadata = { + title: "Templates - Rivet", + description: + "Explore RivetKit templates and examples to quickly start building with Rivet Actors. Find templates for AI agents, real-time apps, games, and more.", + alternates: { + canonical: "https://www.rivet.dev/templates/", + }, +}; + +export default function Page() { + return ; +} diff --git a/website/src/components/v2/Header.tsx b/website/src/components/v2/Header.tsx index cd93bb08c2..253215e147 100644 --- a/website/src/components/v2/Header.tsx +++ b/website/src/components/v2/Header.tsx @@ -254,6 +254,14 @@ export function Header({ > Documentation + + Templates + Documentation + + Templates +