diff --git a/Cargo.lock b/Cargo.lock index c5c7a40680..84e63204dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6068,8 +6068,6 @@ dependencies = [ "async-trait", "base64 0.22.1", "ciborium", - "depot-client", - "depot-client-types", "fs_extra", "futures", "getrandom 0.2.16", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fcd5151d8c..19d42f2346 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3522,6 +3522,9 @@ importers: '@hono/node-server': specifier: ^1.14.1 version: 1.19.9(hono@4.11.9) + '@rivetkit/engine-api-full': + specifier: workspace:* + version: link:../../../engine/sdks/typescript/api-full '@rivetkit/react': specifier: workspace:* version: link:../../../rivetkit-typescript/packages/react diff --git a/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs b/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs index 36fee1c96b..c3c23f031c 100644 --- a/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs +++ b/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs @@ -237,6 +237,8 @@ impl From for ActorConfigInput { .map(|action| rivetkit_core::ActionDefinition { name: action.name }) .collect() }), + // The wasm runtime does not expose custom inspector tabs yet. + inspector_tabs: None, } } } diff --git a/website/scripts/typecheck-staging/package.json b/website/scripts/typecheck-staging/package.json index cd9515f4de..09f66c0b45 100644 --- a/website/scripts/typecheck-staging/package.json +++ b/website/scripts/typecheck-staging/package.json @@ -5,6 +5,7 @@ "dependencies": { "rivetkit": "workspace:*", "@rivetkit/react": "workspace:*", + "@rivetkit/engine-api-full": "workspace:*", "@ai-sdk/openai": "^3.0.30", "ai": "^6.0.92", "hono": "^4.7.0", diff --git a/website/src/components/Footer.jsx b/website/src/components/Footer.jsx index b8e639b883..4b9509b6b8 100644 --- a/website/src/components/Footer.jsx +++ b/website/src/components/Footer.jsx @@ -37,6 +37,7 @@ const footer = { ], resources: [ { name: "Cookbooks", href: "/cookbook" }, + { name: "Compare", href: "/compare" }, { name: "Blog", href: "/blog" }, { name: "YC & Speedrun Deal", href: "/startups" }, { name: "Open-Source Friends", href: "/oss-friends" }, diff --git a/website/src/components/cookbook/CookbookCard.tsx b/website/src/components/cookbook/CookbookCard.tsx index 4919d12a4f..d4fa2ea264 100644 --- a/website/src/components/cookbook/CookbookCard.tsx +++ b/website/src/components/cookbook/CookbookCard.tsx @@ -1,6 +1,7 @@ -import { Icon, faArrowRight } from "@rivet-gg/icons"; +import { CookbookCover } from "./CookbookCover"; export interface CookbookPageCardData { + slug: string; title: string; description: string; href: string; @@ -15,38 +16,13 @@ export interface CookbookPageCardData { }>; } -export function CookbookCard({ page }: { page: CookbookPageCardData }) { +export function CookbookCard({ page, numeral }: { page: CookbookPageCardData; numeral: string }) { return ( - -
-
-

{page.title}

- -
- -

{page.description}

- - {page.templates.length > 0 && ( -
- {page.templates.slice(0, 3).map((t) => ( - - {t.displayName} - - ))} - {page.templates.length > 3 && ( - - +{page.templates.length - 3} - - )} -
- )} -
+
+ ); } diff --git a/website/src/components/cookbook/CookbookCover.tsx b/website/src/components/cookbook/CookbookCover.tsx new file mode 100644 index 0000000000..0455f56f30 --- /dev/null +++ b/website/src/components/cookbook/CookbookCover.tsx @@ -0,0 +1,1231 @@ +import type { ReactNode, SVGProps } from "react"; + +// Poster-style cover art for cookbook entries. Every cover shares one surreal +// dusk-landscape composition: a gradient sky, a haze band, a horizon line over +// a dark sea, a soft reflection column, film grain, and a vignette. Per-cover +// identity comes from the palette and from what hangs in the sky, keyed by the +// entry slug in COVER_ART below. Unknown slugs fall back to a generic floating +// orb scene with a palette picked deterministically from the slug. +// +// `CookbookCoverDefs` must be rendered exactly once on any page that renders +// covers; it holds the shared grain filter, vignette, glints, and edge mask +// that every cover SVG references by id. + +export const COVER_SERIF = '"Perfectly Nineties", Georgia, serif'; + +// Horizon y coordinate in the 500x700 viewBox. +const HZ = 510; + +// Deterministic PRNG so server render and hydration always agree. +function mulberry32(a: number) { + return () => { + a |= 0; + a = (a + 0x6d2b79f5) | 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +const f = (n: number) => Math.round(n * 10) / 10; + +function polar(cx: number, cy: number, r: number, deg: number): [number, number] { + const a = (deg * Math.PI) / 180; + return [f(cx + r * Math.cos(a)), f(cy + r * Math.sin(a))]; +} + +export function toRoman(n: number): string { + const table: Array<[number, string]> = [ + [1000, "M"], + [900, "CM"], + [500, "D"], + [400, "CD"], + [100, "C"], + [90, "XC"], + [50, "L"], + [40, "XL"], + [10, "X"], + [9, "IX"], + [5, "V"], + [4, "IV"], + [1, "I"], + ]; + let out = ""; + for (const [value, symbol] of table) { + while (n >= value) { + out += symbol; + n -= value; + } + } + return out; +} + +type Stop = [number, string, number?]; + +function GradStops({ stops }: { stops: Stop[] }) { + return ( + <> + {stops.map(([offset, color, opacity], i) => ( + + ))} + + ); +} + +function LGrad({ + id, + stops, + ...rest +}: { id: string; stops: Stop[] } & SVGProps) { + return ( + + + + ); +} + +function RGrad({ + id, + stops, + ...rest +}: { id: string; stops: Stop[] } & SVGProps) { + return ( + + + + ); +} + +function HazeDef({ id, color, opacity }: { id: string; color: string; opacity: number }) { + return ( + + ); +} + +const Haze = ({ id }: { id: string }) => ( + +); + +const Sea = ({ id }: { id: string }) => ( + +); + +const HorizonLine = ({ color, opacity }: { color: string; opacity: number }) => ( + +); + +const Glints = ({ color }: { color: string }) => ( + + + +); + +function Figure({ x, h = 14, w = 3 }: { x: number; h?: number; w?: number }) { + const y = HZ - h; + return ( + <> + + + + ); +} + +function Stone({ x, h = 12, w = 4 }: { x: number; h?: number; w?: number }) { + return ( + + ); +} + +function Stars({ + seed, + color, + n, + yMin = 36, + yMax = 360, +}: { + seed: number; + color: string; + n: number; + yMin?: number; + yMax?: number; +}) { + const r = mulberry32(seed); + const items: ReactNode[] = []; + for (let i = 0; i < n; i++) { + const x = f(18 + r() * 464); + const y = f(yMin + r() * (yMax - yMin)); + const rad = f(0.6 + r() * 1.0); + const o = f(0.08 + r() * 0.22); + items.push(); + } + return <>{items}; +} + +const GLINT_ROWS: Array<[number, number, number, number]> = [ + [524, 178, 322, 0.5], + [531, 118, 252, 0.42], + [540, 258, 402, 0.36], + [551, 58, 192, 0.3], + [561, 298, 458, 0.27], + [574, 148, 332, 0.23], + [589, 38, 152, 0.19], + [603, 228, 412, 0.16], + [621, 88, 222, 0.13], + [639, 278, 432, 0.1], + [661, 138, 302, 0.08], + [680, 230, 360, 0.06], +]; + +// Shared defs referenced by every cover SVG. Render once per page. +export function CookbookCoverDefs() { + return ( + + ); +} + +// A memory orb raining light streams over a violet sea. +function AiAgentArt() { + const r = mulberry32(11); + const streams: ReactNode[] = []; + for (let i = 0; i < 16; i++) { + const x = f(168 + r() * 164); + const y1 = f(334 + r() * 24); + const y2 = f(y1 + 60 + r() * 120); + streams.push( + , + ); + } + return ( + <> + + + + + + + + + + + + + + + + + + {streams} + + + + +
+ + ); +} + +// Slabs ascend in an arc from a grounded monolith toward the upper right. +function WorkspacesArt() { + const slabs: Array<[number, number, number, number]> = [ + [126, 338, 68, 94], + [244, 286, 54, 76], + [312, 224, 42, 60], + [402, 248, 30, 44], + [428, 168, 20, 30], + [180, 420, 28, 40], + ]; + return ( + <> + + + + + + + + + + + + + + {slabs.map(([x, y, w, h], i) => { + const cx = f(x + w / 2); + return ( + + + + + + + + ); + })} + + + + + + + + + + ); +} + +// Broadcast ripple rings with member dots over a teal sea. +function ChatRoomArt() { + const cx = 250; + const cy = 396; + const rings: Array<[number, number]> = [ + [38, 0.5], + [76, 0.33], + [122, 0.2], + [176, 0.12], + [240, 0.07], + ]; + const dots: Array<[number, number, number]> = [ + [76, 205, 3.4], + [76, 332, 3], + [122, 158, 3], + [122, 22, 3.6], + [122, 262, 2.6], + [38, 120, 2.4], + [176, 196, 2.6], + [176, 348, 2.2], + ]; + const ripples: Array<[number, number, number]> = [ + [46, 7, 0.28], + [90, 13, 0.16], + [150, 22, 0.09], + ]; + return ( + <> + + + + + + + + + + + {rings.map(([r, o], i) => ( + + ))} + + + {dots.map(([rr, a, rad], i) => { + const [x, y] = polar(cx, cy, rr, a); + return ; + })} + + + {ripples.map(([rx, ry, o], i) => ( + + ))} + + + + + + + ); +} + +// Rays from every collaborator converge on a single glowing caret. +function CollabEditorArt() { + const pt: [number, number] = [250, 504]; + const edges: Array<[number, number]> = []; + for (let x = 16; x <= 484; x += 52) edges.push([x, 0]); + for (let y = 120; y <= 460; y += 85) { + edges.push([0, y]); + edges.push([500, y]); + } + return ( + <> + + + + + + + + + + + + {edges.map(([x, y], i) => ( + + ))} + + + + + + + + + + + + +
+
+ + ); +} + +// A half-risen dial sun with tick marks burning on the horizon. +function CronArt() { + const cx = 250; + const cy = 512; + const ticks: ReactNode[] = []; + for (let a = 184; a <= 356; a += 4) { + const major = a % 20 === 0; + const r1 = major ? 108 : 114; + const r2 = major ? 130 : 123; + const [x1, y1] = polar(cx, cy, r1, a); + const [x2, y2] = polar(cx, cy, r2, a); + ticks.push( + , + ); + } + const [hx1, hy1] = polar(cx, cy, 104, 305); + const [hx2, hy2] = polar(cx, cy, 142, 305); + const stoneXs = [55, 120, 185, 250, 315, 380, 445]; + return ( + <> + + + + + + + + + + + + {ticks} + + + + + + + + + + + {stoneXs.map((x, i) => ( + + ))} + + ); +} + +// A constellation of dart cursors with trails in a slate blue sky. +function CursorsArt() { + const darts: Array<[number, number, number, number, number]> = [ + [140, 252, -32, 1.1, 0.9], + [305, 224, 42, 0.9, 0.72], + [228, 332, 12, 1.35, 0.95], + [352, 318, -16, 0.8, 0.6], + [96, 350, 56, 0.7, 0.5], + [262, 410, -52, 0.9, 0.7], + [180, 434, 26, 0.62, 0.45], + [398, 402, 16, 0.62, 0.4], + [320, 462, -30, 0.5, 0.35], + ]; + const links: Array<[number, number, number, number]> = [ + [140, 252, 305, 224], + [228, 332, 140, 252], + [228, 332, 352, 318], + [262, 410, 228, 332], + ]; + return ( + <> + + + + + + + + + + + + + {links.map(([x1, y1, x2, y2], i) => ( + + ))} + {darts.map(([x, y, rot, s, o], i) => { + const a = ((rot + 90) * Math.PI) / 180; + const len = 26 + s * 30; + const tx1 = f(x + Math.cos(a) * 9 * s); + const ty1 = f(y + Math.sin(a) * 9 * s); + const tx2 = f(x + Math.cos(a) * (9 * s + len)); + const ty2 = f(y + Math.sin(a) * (9 * s + len)); + return ( + + ); + })} + {darts.map(([x, y, rot, s, o], i) => ( + + ))} + + + + +
+ + ); +} + +// A ringed sphere with a moon over a perspective grid plain. +function GameArt() { + const horiz: Array<[number, number]> = [ + [514, 0.35], + [521, 0.3], + [531, 0.26], + [545, 0.22], + [564, 0.18], + [590, 0.14], + [624, 0.11], + [668, 0.08], + ]; + const radial = [-140, -40, 40, 120, 200, 250, 300, 380, 460, 540, 640]; + return ( + <> + + + + + + + + + + + + + + + + + + + + {horiz.map(([y, o], i) => ( + + ))} + {radial.map((x, i) => ( + + ))} + + + + + ); +} + +// Five identical sealed vaults, each under its own beam of light. +function TenantsArt() { + const centers = [90, 170, 250, 330, 410]; + return ( + <> + + + + + + + + + + + + + {centers.map((cx) => ( + + ))} + {centers.map((cx) => { + const x = cx - 16; + return ( + + + + + + + ); + })} + + {centers.map((cx) => ( + + ))} + + + + ); +} + +const FALLBACK_PALETTES = [ + { + sky: [ + [0, "#050b11"], + [0.45, "#0c1c28"], + [0.78, "#173448"], + [1, "#22506b"], + ] as Stop[], + body: "#9cc4de", + glow: "#7fb2d4", + haze: "#6f9cc4", + sea: [ + [0, "#13283a"], + [0.5, "#071420"], + [1, "#010204"], + ] as Stop[], + glint: "#2e4f6b", + horizon: "#9dc1dd", + }, + { + sky: [ + [0, "#0a0508"], + [0.45, "#1d0d18"], + [0.78, "#3a1a30"], + [1, "#5b2a48"], + ] as Stop[], + body: "#e0a8c8", + glow: "#c486ab", + haze: "#b07697", + sea: [ + [0, "#311527"], + [0.5, "#130810"], + [1, "#030102"], + ] as Stop[], + glint: "#5e3049", + horizon: "#dba4c4", + }, + { + sky: [ + [0, "#0b0903"], + [0.45, "#1f1a08"], + [0.78, "#3d3411"], + [1, "#5e511c"], + ] as Stop[], + body: "#e6d49a", + glow: "#cdb878", + haze: "#b3a065", + sea: [ + [0, "#2e2810"], + [0.5, "#120f06"], + [1, "#020201"], + ] as Stop[], + glint: "#5c5126", + horizon: "#dccb93", + }, + { + sky: [ + [0, "#040810"], + [0.45, "#0d1430"], + [0.78, "#1b2756"], + [1, "#2c3c7a"], + ] as Stop[], + body: "#aebcf0", + glow: "#8d9fdd", + haze: "#7c8cc8", + sea: [ + [0, "#18204a"], + [0.5, "#0a0d20"], + [1, "#020205"], + ] as Stop[], + glint: "#36406e", + horizon: "#a9b8ea", + }, +]; + +function hashSlug(slug: string): number { + let h = 0; + for (let i = 0; i < slug.length; i++) { + h = (h * 31 + slug.charCodeAt(i)) | 0; + } + return Math.abs(h); +} + +// Generic floating orb scene for entries without bespoke artwork. +function FallbackArt({ slug }: { slug: string }) { + const hash = hashSlug(slug); + const palette = FALLBACK_PALETTES[hash % FALLBACK_PALETTES.length]; + const id = (part: string) => `fb-${slug}-${part}`; + return ( + <> + + + + + + + + + + + + + + + + + + + +
+ + ); +} + +const COVER_ART: Record ReactNode> = { + "ai-agent": AiAgentArt, + "ai-agent-workspace": WorkspacesArt, + "chat-room": ChatRoomArt, + "collaborative-text-editor": CollabEditorArt, + "cron-jobs": CronArt, + "live-cursors": CursorsArt, + "multiplayer-game": GameArt, + "per-tenant-database": TenantsArt, +}; + +function RivetMark({ className }: { className?: string }) { + return ( + + ); +} + +export interface CookbookCoverProps { + slug: string; + title: string; + numeral: string; +} + +// Renders the full poster inside a parent that must be `relative`, +// `overflow-hidden`, sized to aspect 5/7, have `container-type: inline-size`, +// and carry the `group` class for the hover treatment. +export function CookbookCover({ slug, title, numeral }: CookbookCoverProps) { + const Art = COVER_ART[slug]; + const longTitle = title.length > 18; + return ( + <> + +
+ +

+ {title} +

+
+
+ +
+ + ); +} diff --git a/website/src/components/cookbook/CookbookPageContent.tsx b/website/src/components/cookbook/CookbookPageContent.tsx index 9525128069..0946b54337 100644 --- a/website/src/components/cookbook/CookbookPageContent.tsx +++ b/website/src/components/cookbook/CookbookPageContent.tsx @@ -4,6 +4,7 @@ import { useMemo, useState } from "react"; import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { TECHNOLOGIES, TAGS } from "@/data/templates/shared"; import { CookbookCard, type CookbookPageCardData } from "./CookbookCard"; +import { CookbookCoverDefs, COVER_SERIF, toRoman } from "./CookbookCover"; export interface CookbookPageListItem extends CookbookPageCardData { tags: string[]; @@ -108,15 +109,26 @@ export function CookbookPageContent({ pages, allTags, allTechnologies }: Cookboo const hasActiveFilters = selectedTags.length > 0 || selectedTechnologies.length > 0 || searchQuery.trim().length > 0; + // Numerals come from the full alphabetical list so filtering never renumbers covers. + const numerals = useMemo( + () => new Map(pages.map((p, i) => [p.slug, toRoman(i + 1)])), + [pages], + ); + const toggle = (list: string[], setList: (v: string[]) => void, item: string) => { setList(list.includes(item) ? list.filter((x) => x !== item) : [...list, item]); }; return (
+
-
-

+
+

Rivet

+

Cookbooks

@@ -126,7 +138,7 @@ export function CookbookPageContent({ pages, allTags, allTechnologies }: Cookboo

-
+
@@ -169,9 +181,9 @@ export function CookbookPageContent({ pages, allTags, allTechnologies }: Cookboo {filteredPages.length === 0 ? (
No guides found matching your filters
) : ( -
+
{filteredPages.map((page) => ( - + ))}
)} diff --git a/website/src/components/faq/FaqJsonLd.astro b/website/src/components/faq/FaqJsonLd.astro new file mode 100644 index 0000000000..e4ef1c77e7 --- /dev/null +++ b/website/src/components/faq/FaqJsonLd.astro @@ -0,0 +1,25 @@ +--- +// Emits FAQPage JSON-LD for a page's FAQ items. Usage rules: +// - At most one FaqJsonLd per page, and only from .astro pages, so each URL +// has exactly one FAQPage block. +// - The same items array must also be rendered visibly on the page through +// FaqList or FaqSection. Google requires schema content to be visible. +// - FAQPage coexists with Product, Article, and other schema types as sibling +// script tags. Never nest it inside another schema object. +// - Since August 2023 Google shows FAQ rich results only for authoritative +// government and health sites. The value here is long-tail content and +// AI-answer extraction; the schema is nearly free since it derives from the +// same data module as the visible rendering. +import { faqPageSchema, type FaqItem } from '@/data/faqs/types'; +import { jsonLdString } from '@/lib/jsonLd'; + +interface Props { + items: FaqItem[]; +} + +const { items } = Astro.props; + +const json = jsonLdString(faqPageSchema(items)); +--- + +