diff --git a/CLAUDE.md b/CLAUDE.md
index d198d2a7..791d9964 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -17,7 +17,7 @@ Applies to all user-facing writing on the website (docs, marketing, blog). Inter
## Docs Routing
-- The site-wide docs overview is `/docs/`; the Guides tab is `/guides/` (the Actors bundle's `learn` section plus website-owned guides in `src/content/guides/`); the Deploy tab is `/docs/deploy/{self-host,byoc,cloud}/`. Product documentation stays at `/{product}/docs/` and `/{product}/integrations/`.
+- The site-wide docs overview is `/docs/`; the Guides tab is `/guides/` (every product bundle's `content/guides/` and `guides` sidebar key, merged in product order with same-titled groups combined; the website owns only the overview in `src/content/guides/index.mdx`); the Deploy tab is `/docs/deploy/{self-host,byoc,cloud}/`. Product documentation stays at `/{product}/docs/` and `/{product}/integrations/`.
- The route prefixes live in `src/sitemap/deploy.ts` and `src/sitemap/guides.ts`. Derive hrefs from them; never hand-write `/orchestrate/`, `/{product}/self-host/`, or `/actors/learn/`, which are redirects.
## Docs Pages
diff --git a/DARK_MODE_ASSETS.md b/DARK_MODE_ASSETS.md
index 791d6622..6df77d1a 100644
--- a/DARK_MODE_ASSETS.md
+++ b/DARK_MODE_ASSETS.md
@@ -24,7 +24,7 @@ Run `node scripts/check-theme.mjs` against a dev server to verify the CSS-handle
| Product wordmarks `src/images/products/*-logo.svg` | `ProductBadge`, `ProductLockup`, `StackSection` | Always white inside an ink or accent tile (AGENTS.md rule). No change needed. |
| Inline SVG diagrams that hardcode the light palette (`#1b1916`, `#56524a`, `#8a8578`, `#2E4034`, `#ffffff`, `#faf8f3`, `#e7ece7`) as presentation attributes | Vendored docs under `vendor/*/docs` (versions, tracing, dynamic-apps, agentOS architecture, quickstarts) and dated blog posts | `theme.css` remaps each hex to its dark token with `[fill="…" i]` / `[stroke="…" i]` selectors inside `.docs-article` and `.blog-article`; CSS wins over a presentation attribute. Surface remaps are guarded by `svg:has(ink-or-pine)` so they never touch another palette. Website-owned diagrams (`src/components/docs/*Diagram.astro`, `src/content/self-host/**`) were converted to `rgb(var(--site-*, fallback))` tokens instead and need no remap. |
| Inline SVG diagrams drawn in a foreign palette (agentOS `security-model`, `architecture/posix-syscalls`, `architecture/packages-and-command-resolution` use Tailwind zinc/slate/indigo/emerald) | `vendor/agentos/docs` | `theme.css` applies `invert(1) hue-rotate(180deg)` to any `.docs-article svg[role="img"]` that uses neither tokens, `currentColor`, nor the site palette. |
-| Excalidraw PNG diagrams on an opaque white plate (`assets.rivet.dev/website/docs/general/runtime-modes/*.png`, `endpoints/endpoint-env-vars.png`, `website/learn/act-1/scene-1/*.png`) | `vendor/docs/docs/content/runtime-modes.mdx`, `endpoints.mdx`; `src/content/guides/a-radically-simpler-architecture.mdx` | `theme.css` applies `invert(1) hue-rotate(180deg)` to `.docs-article img[alt*="diagram" i]` and to `.theme-diagram-invert`. Vendored `
`s match on their alt text; the two guide figures whose alt lacks "diagram" carry the class. Screenshots never match. |
+| Excalidraw PNG diagrams on an opaque white plate (`assets.rivet.dev/website/docs/general/runtime-modes/*.png`, `endpoints/endpoint-env-vars.png`, `website/learn/act-1/scene-1/*.png`) | `vendor/docs/docs/content/runtime-modes.mdx`, `endpoints.mdx`; `vendor/actors/docs/content/guides/a-radically-simpler-architecture.mdx` | `theme.css` applies `invert(1) hue-rotate(180deg)` to `.docs-article img[alt*="diagram" i]` and to `.theme-diagram-invert`. Vendored `
`s match on their alt text; the two guide figures whose alt lacks "diagram" carry the class. Screenshots never match. |
| Mermaid fences (`pre.mermaid`, only in `src/content/posts/2026-06-17-introducing-the-rust-sdk/page.mdx` outside the Learn section) | `MermaidScript.astro` | Picks `neutral`/`dark` from the background luminance at render, keeps the source in `data-mermaid-source`, and re-renders on the `theme-change` event so a toggle does not leave a stale light diagram. |
## Intentionally unchanged
diff --git a/HIDDEN.md b/HIDDEN.md
index 7122e685..82434f49 100644
--- a/HIDDEN.md
+++ b/HIDDEN.md
@@ -12,19 +12,10 @@ working — they are just unreachable from the nav.
| Product | Tab | Why | To restore |
| --- | --- | --- | --- |
| All | Use Cases | The other three are stubs, and agentOS retired its page (`/agentos/use-cases/` redirects to `/guides/`) | Write them, add `"use-cases"` back to each `tabs` |
-| Actors | Learn | Cookbooks are thin and the section has no landing copy | Add `"learn"` to `optionalTabs` and `tabs` |
| agentOS | Learn | Was generated from `examples/*/README.md` by a loader that did not survive the split. Content deleted; READMEs still in `~/agentos/examples/` | Port the loader or convert the READMEs to MDX |
| Dynamic Apps | Learn | Placeholder only, deleted | Write it |
| Workflows | Learn | Placeholder only, deleted | Write it |
-### Pages behind the hidden Actors Learn tab
-
-Still routed at `/actors/learn/*`, unreachable from the nav:
-
-- `a-radically-simpler-architecture` — a full essay, the strongest piece here
-- `ai-agent`, `chat-room`, `collaborative-text-editor`, `cron-jobs`,
- `live-cursors`, `multiplayer-game`, `per-tenant-database` — cookbooks
-
## Hidden products
| Product | Why |
diff --git a/redirects.mjs b/redirects.mjs
index 63b68c60..122fb271 100644
--- a/redirects.mjs
+++ b/redirects.mjs
@@ -341,13 +341,12 @@ function selfHostRedirects() {
return map;
}
-// The Actors bundle's `learn` section rendered at `/actors/learn/...` before
-// it became the site-wide Guides tab at `/guides/...`. Website-owned guides in
-// `src/content/guides` never had another URL, but are included so the map
-// lists every guide.
+// The Actors bundle's guides rendered at `/actors/learn/...` before they moved
+// to the site-wide Guides tab at `/guides/...`. Guides from other bundles never
+// had another URL.
function guidesRedirects() {
const map = {};
- for (const slug of mdxSlugs(path.join(CONTENT_ROOT, 'docs/actors/learn'))) {
+ for (const slug of mdxSlugs(path.join(CONTENT_ROOT, 'docs/actors/guides'))) {
map[slug ? `/actors/learn/${slug}` : '/actors/learn'] = slug ? `/guides/${slug}/` : '/guides/';
}
return map;
diff --git a/scripts/check-sitemap.ts b/scripts/check-sitemap.ts
index e026e287..d8ae2f64 100644
--- a/scripts/check-sitemap.ts
+++ b/scripts/check-sitemap.ts
@@ -13,8 +13,8 @@
* 3. Every product docs/integrations sidebar href resolves to a content file.
* 4. Every sidebar href appears in exactly one tab.
* 5. Every routed, indexable product-doc page has a sidebar or content inlink.
- * 6. Every Guides sidebar href resolves to an Actors `learn` page or a
- * website-owned guide.
+ * 6. The Guides sidebar and every bundle's `guides` content directory agree
+ * exactly, and no two bundles define the same guide slug.
* 7. The site Integrations sidebar (`/integrations/`) and the Actors
* `integrations` content directory agree exactly.
*
@@ -37,9 +37,9 @@ import {
} from "../src/sitemap/deploy";
import { selfHostGuides } from "../src/sitemap/deployMatrix";
import {
- ACTORS_LEARN_CONTENT_PREFIX,
+ GUIDE_BUNDLES,
GUIDES_ROUTE_PREFIX,
- SITE_GUIDES,
+ GUIDES_SECTION,
} from "../src/sitemap/guides";
import { integrationSidebar } from "../src/data/integrations";
import {
@@ -279,9 +279,32 @@ for (const product of products) {
}
}
-// 6. Every Guides sidebar href is an Actors `learn` page or a website guide.
+// 6. The Guides sidebar and every bundle's `guides` content directory agree
+// exactly: every href has one content file, every guide file has a sidebar
+// link, and no two bundles define the same slug.
{
- const learnContent = path.join(DOCS_CONTENT, ACTORS_LEARN_CONTENT_PREFIX);
+ const owners = new Map();
+ for (const bundle of GUIDE_BUNDLES) {
+ const base = path.join(DOCS_CONTENT, bundle, GUIDES_SECTION);
+ for (const file of fg.sync("**/*.mdx", { cwd: base, followSymbolicLinks: true })) {
+ const slug = file.replace(/\.mdx$/, "").replace(/(^|\/)index$/, "");
+ if (!slug) {
+ errors.push(
+ `${bundle}/${GUIDES_SECTION}/index.mdx is never rendered; the Guides overview is website-owned`,
+ );
+ continue;
+ }
+ owners.set(slug, [...(owners.get(slug) ?? []), bundle]);
+ }
+ }
+ for (const [slug, bundles] of owners) {
+ if (bundles.length > 1) {
+ errors.push(
+ `${GUIDES_ROUTE_PREFIX}/${slug}/ is defined by more than one bundle: ${bundles.join(", ")}`,
+ );
+ }
+ }
+ const sidebarSlugs = new Set();
for (const href of collectHrefs(guidesSidebar())) {
if (!href.startsWith(`${GUIDES_ROUTE_PREFIX}/`)) {
errors.push(
@@ -292,16 +315,21 @@ for (const product of products) {
const slug = href
.slice(`${GUIDES_ROUTE_PREFIX}/`.length)
.replace(/\/$/, "");
- // The Guides overview is website-owned; the Actors bundle carries only
- // the worked examples under it.
- const siteGuide = slug
- ? SITE_GUIDES.some((guide) => guide.slug === slug) &&
- contentFileExists(GUIDES_CONTENT, slug)
+ sidebarSlugs.add(slug);
+ const exists = slug
+ ? owners.has(slug)
: contentFileExists(GUIDES_CONTENT, "index");
- if (!siteGuide && !contentFileExists(learnContent, slug)) {
+ if (!exists) {
errors.push(`Guides sidebar links ${href}, which has no content file`);
}
}
+ for (const [slug, bundles] of owners) {
+ if (!sidebarSlugs.has(slug)) {
+ errors.push(
+ `${bundles[0]}/${GUIDES_SECTION}/${slug}.mdx is not linked from that bundle's "guides" sidebar`,
+ );
+ }
+ }
}
// 7. The site Integrations sidebar and the Actors `integrations` content
diff --git a/src/components/docs/GuideCards.tsx b/src/components/docs/GuideCards.tsx
index edaca41b..6d76ec12 100644
--- a/src/components/docs/GuideCards.tsx
+++ b/src/components/docs/GuideCards.tsx
@@ -35,7 +35,7 @@ function guideGroups(overviewHref: string): GuideGroup[] {
/**
* The Guides overview, generated from the same sidebar the tab is built from:
* one heading per sidebar section, then a card per guide in it. Adding a guide
- * to the Actors bundle's sidebar or to `SITE_GUIDES` updates both.
+ * to any bundle's `guides` sidebar updates both.
*
* `descriptions` maps each guide href to its frontmatter description; the
* route supplies it from the content collections.
diff --git a/src/content/guides/a-radically-simpler-architecture.mdx b/src/content/guides/a-radically-simpler-architecture.mdx
deleted file mode 100644
index 443a5c80..00000000
--- a/src/content/guides/a-radically-simpler-architecture.mdx
+++ /dev/null
@@ -1,136 +0,0 @@
----
-title: "A Radically Simpler Architecture"
-description: "Why actors eliminate complexity instead of managing it, and how merging state and compute removes the biggest source of latency in modern applications."
----
-
-The typical backend architecture follows a familiar pattern. A web server connects to a database. Traffic grows, so you add Redis for caching. You need async processing, so you add Kafka. You need coordination, so you add distributed locks.
-
-## Every Solution Creates A Problem
-
-As you add components to your architecture to solve problems as you scale, in turn you create new problems for yourself:
-
-- **Caching**: brings cache invalidation bugs, stale data, and thundering herd problems.
-- **Message queues**: bring message ordering issues, exactly-once delivery problems, and dead letter queue monitoring.
-- **Pub/sub systems**: bring subscription management complexity, message replay challenges, and coordination overhead.
-- **Distributed locks**: bring deadlocks, lock timeouts, and split-brain scenarios.
-- **Multiple services**: bring distributed transactions, eventual consistency, and network partition handling.
-
-Worse, these are bugs you can't unit test for. They're emergent behaviors that only appear under load when it matters most.
-
-Yet it's accepted as _the way things must be_. Nobody got fired for adding Kafka, Redis, and RabbitMQ to the stack. The fact that each one brings its own failure modes is assumed to be the growing pains of any successful business.
-
-
-
----
-
-## How We Got Here
-
-Looking back at the very first thing you did when starting your application: setting up a web server and a database. The way you've designed your app is through an **age-old practice of "separating state and compute."**
-
-We've been doing it this way since the 1980s, when client-server architecture put databases on their own machines.
-
-This came from the fact that computers were slow and had limited resources. Running application code and database operations on the same machine meant they'd fight over CPU and memory, making both perform poorly. Separating them protected databases from compute overhead.
-
-This tradeoff made sense when CPU and memory were severely limited, but the pattern outlived its purpose. As traffic grew, we added caching layers, message queues, and distributed locks — each solving a problem from the last without questioning the original assumption of how we got here.
-
-## 40 Years Later
-
-Those constraints from forty years ago no longer apply to today's servers. Modern CPUs are orders of magnitude faster, and memory is abundant and cheap. **Application bottlenecks have shifted from local compute to network latency and locks.**
-
-This is best demonstrated with a simple comparison between a real-world Postgres query over the network versus a SQLite query on the same machine: A Postgres query over the network takes 1-10ms over LAN. The same query on a local SQLite database running in the same process as your application takes 0.01-0.1ms, **roughly 100x faster**. (These benchmarks are heavily dependent on the workload, this is a conservative performance number for SQLite.)
-
-That 100x difference is not about switching to a marginally different database, it's about rethinking your architecture for modern computers by eliminating the centralized database completely in favor of databases colocated with your compute. **Combining compute and state removes the biggest sources of latency in modern applications.**
-
----
-
-## The Actor Model: Combining Compute and State
-
-Actors take the completely opposite approach to "separating compute and state:" they **merge state and compute together**.
-
-Each actor's **state is isolated to itself** and cannot be read by any other actors. Instead, you communicate with actors over the network via actions.
-
-They're like mini-servers: they can accept and respond to network requests and even send network requests themselves. They remain running as a long-lived process with in-memory state until they decide to go to sleep.
-
-In addition to performance and complexity benefits, this architecture **eliminates entire categories of bugs by design.** No network to the database means no network partitions. No shared state means no race conditions. No locks means no deadlocks.
-
-
-
-## The 4 Properties That Eliminate Complexity
-
-By combining compute and state, actors present a few key properties that eliminate entire categories of problems. These properties are the core of the design patterns that we'll discuss in further articles.
-
-### Isolated State
-
-Each actor **manages its own private state**. No other process can access an actor's state.
-
-This eliminates race conditions (can't happen when only one process touches the data), deadlocks (no locks means no deadlocks), cache invalidation (no shared cache to invalidate), and read-after-write inconsistencies (your writes are immediately visible to you).
-
-Debugging becomes straightforward: the actor's state is the single source of truth. There's no need to reconstruct state from multiple systems or reason about eventual consistency across caches, databases, and message queues.
-
-As your app grows, new features affect a limited number of actors which have a limited scope. Changes don't ripple through shared state across services or risk breaking unrelated parts of your system.
-
-
-
-### Message-Based Communication
-
-Actors **talk through actions and events**, not direct state access. This makes it easier to scale actors since they can scale horizontally across multiple machines and still communicate efficiently.
-
-Messages sent to actors are **automatically queued and processed sequentially**. This almost always eliminates the need for external message queues since backpressure, ordering, and delivery are handled by the actor runtime itself.
-
-Crucially, **actors frequently talk to each other** to build larger systems that scale well. We'll be talking a lot about patterns like this in this course.
-
-
-
-### Location Transparency
-
-Actors can run on any machine in a cluster and still **send messages between actors regardless of the host machine**. Rivet automatically handles intelligent load balancing of actors and routing between actors.
-
-The same code will run whether you have 1 or 1,000 machines without complex network configuration, DNS, or pub/sub systems.
-
-
-
-### Horizontal Scaling
-
-Actors are designed to transparently interact with other actors regardless of what machine they run on. This makes actors easy to scale by **just adding more machines** for actors to run on when you need it.
-
-Load spreads naturally since actors are small, lightweight units. No complex sharding logic or coordination needed.
-
-
-
----
-
-## Putting It All Together: A Radically Simpler Architecture
-
-When you build your backend with actors, the four properties listed remove the need for:
-
-- **Redis/Memcached**: Caching is built-in (state already lives in-memory with compute).
-- **Kafka/RabbitMQ/SQS**: Message queueing, events, and async messaging are built-in to the actor runtime.
-- **NATS/Redis Streams**: Pub/sub is built-in to actors through message passing and events.
-- **Consul/etcd/ZooKeeper**: No distributed coordination needed, actors encapsulate their own state and the runtime handles discovery and routing automatically.
-- **Istio/Linkerd**: Actors handle routing and discovery automatically.
-- **Database sharding**: Actors distribute themselves automatically. No shard keys, no rebalancing logic, no cross-shard queries.
-
-## If Actors Are So Great, Why Aren't They Everywhere?
-
-If you've reached this point and are unfamiliar with the actor model, you're probably asking this exact question. It all sounds a little _too_ rosy.
-
-The truth is that actors _are_ used widely — just not visibly. Large enterprises with engineers who've spent years wrestling with traditional architectures have long since adopted them. The pattern has proven itself at massive scale:
-
-- WhatsApp (notoriously acquired for $19B running Erlang/OTP with only 35 engineers)
-- Discord
-- LinkedIn
-- X
-- Pinterest
-- PayPal
-- FoundationDB (powering Apple, Snowflake, DataDog)
-
-So why hasn't the actor model spread to smaller teams and mainstream development?
-
-This mirrors TypeScript's trajectory. It started as a niche tool for large codebases — most developers dismissed it as unnecessary overhead with poor tooling. But as more developers felt the pain of loose typing at scale, adoption grew. Today, TypeScript is a non-negotiable for many teams because of that collective suffering.
-
-Actors are on the same trajectory. The pain of distributed systems complexity is becoming impossible to ignore.
-
-Other ecosystems have had mature actor frameworks for years — Erlang has OTP, Java has Akka, C# has Microsoft Orleans. But TypeScript has been the missing piece until recently with:
-
-- **Rivet Actors**: Open-source actor infrastructure for TypeScript
-- **Cloudflare Durable Objects**: Leverages Cloudflare's existing network & JavaScript runtime
diff --git a/src/content/guides/agent-app-builders.mdx b/src/content/guides/agent-app-builders.mdx
deleted file mode 100644
index d2b6ea51..00000000
--- a/src/content/guides/agent-app-builders.mdx
+++ /dev/null
@@ -1,29 +0,0 @@
----
-title: "Agent App Builders"
-description: "Ship a backend per user the moment the agent generates one, on Rivet Actors."
----
-
-When your product's output is an app, the backend has to appear the moment the agent writes it — one per user, isolated from every other user, live enough to click on in the same chat turn. Rivet gives each generated app its own Actor and its own persistence, so provisioning is a key lookup rather than a deploy pipeline.
-
-## Two shapes, depending on whose code it is
-
-**Run the generated code in place.** A code agent Actor keeps the chat history and the current revision of the generated source in its own [SQLite database](/actors/docs/sqlite), so every key has an isolated transcript and an isolated codebase. A dynamic Actor fetches that source from the matching code agent and executes it in a [Secure Exec](/secure-exec) sandbox, so the user can call their app the moment it compiles and iterate on it without a redeploy.
-
-**Deploy it into its own namespace.** For code that should outlive the chat, create a Rivet namespace per user, package the generated `registry.ts` and frontend into a project, deploy it to a serverless host such as [Freestyle](/docs/deploy/self-host/workers/freestyle), and configure that host as the namespace's worker. Each user's Actors then run under their own credentials, in their own namespace, with nothing shared but the control plane.
-
-## Start from the examples
-
-
-
- Chat, a Monaco editor, and a live test panel: generate Actor code, persist it in per-Actor SQLite, and invoke it in an isolated dynamic runtime.
-
-
- Provision a namespace per user, package the generated source, and deploy it to Freestyle as that namespace's worker.
-
-
-
-## Next steps
-
-- [Dynamic Apps](/dynamic-apps/docs) — the managed version of this pattern: deploy a generated app and backend per user, with SQLite, workflows, and multiplayer built in.
-- [Actor Keys](/actors/docs/keys) — how one key per user becomes one backend per user.
-- [Deploy](/docs/deploy/) — Rivet Cloud, your own cloud, or self-hosted.
diff --git a/src/content/guides/coding-agents.mdx b/src/content/guides/coding-agents.mdx
deleted file mode 100644
index 1e037cd4..00000000
--- a/src/content/guides/coding-agents.mdx
+++ /dev/null
@@ -1,28 +0,0 @@
----
-title: "Coding Agents"
-description: "Give each coding session a sandbox, a filesystem, and durable memory on Rivet Actors."
----
-
-A coding agent needs somewhere to run commands and somewhere to remember what it already did. On Rivet both belong to the same object: one Actor per coding session, holding the transcript and owning a sandbox whose filesystem survives between turns.
-
-## One Actor per session
-
-Key the agent Actor by session id, so `agent.getOrCreate([sessionId])` always reaches the same conversation (see [Actor Keys](/actors/docs/keys)). The transcript, the run status, and the sandbox session id live in that Actor's persistent [state](/actors/docs/state), so a restart resumes the session instead of starting a new one. There is no separate session database to keep in sync.
-
-## The sandbox is an Actor too
-
-The sandbox comes from `rivetkit/sandbox` and shares the agent's key, which makes the agent-to-sandbox mapping implicit in the key space. It runs the coding agent — Codex by default — inside Docker, Daytona, or E2B, and owns the filesystem and process state for that session. The agent Actor submits a prompt, awaits the sandbox round trip, and broadcasts the result to connected clients as an [event](/actors/docs/events).
-
-## Start from the example
-
-
-
- A React chat UI backed by a `rivetkit/sandbox` Actor: one sandbox, filesystem, and resumable session per coding agent.
-
-
-
-## Next steps
-
-- [AI Agent](/guides/ai-agent) — memory, queued message handling, and streaming responses in depth.
-- [Sandboxes](/agentos/docs/sandboxes) — what a sandbox can run and how it is isolated.
-- [Deploy](/docs/deploy/) — run this on Rivet Cloud, in your own cloud, or self-hosted.
diff --git a/src/content/guides/company-agents.mdx b/src/content/guides/company-agents.mdx
deleted file mode 100644
index bc3e2299..00000000
--- a/src/content/guides/company-agents.mdx
+++ /dev/null
@@ -1,33 +0,0 @@
----
-title: "Company-Specific Agents"
-description: "Run agents against internal data, inside your own network, on Rivet Actors."
----
-
-An agent that works on internal data is constrained less by the model than by where the data is allowed to go. Rivet answers that by keeping each agent's memory in the agent itself and letting you run the whole control plane inside your own network.
-
-## Memory lives in the agent
-
-Give every conversation its own Actor, keyed by agent or conversation id. The transcript and status live in the Actor's persistent [state](/actors/docs/state), and each model call rebuilds the prompt from that state plus a system prompt — memory and inference input are the same data, with no vector store or session table beside it. Prompts arrive on the Actor's [queue](/actors/docs/queues) and the `run` hook consumes them serially, so one conversation never has two model calls in flight. Tokens stream back to clients as [events](/actors/docs/events).
-
-Because the memory is the Actor, the blast radius of any single agent is one Actor: it can only read what its own tools hand it, and its transcript is never pooled with another tenant's.
-
-## It runs where your data is
-
-Internal agents usually cannot call out to a vendor's control plane. Two deployments keep everything inside your perimeter:
-
-- [Bring Your Own Cloud](/docs/deploy/byoc/) — Rivet deploys and operates the control plane inside your VPC. No inbound management connection, and you choose whether it is reachable publicly or only privately.
-- [Self-host](/docs/deploy/self-host/control-plane/) — you run the control plane yourself on Kubernetes, ECS, Docker Compose, or a VM.
-
-## Start from the example
-
-
-
- Queue-driven Actor agents with streaming Vercel AI SDK responses, one Actor per agent.
-
-
-
-## Next steps
-
-- [AI Agent](/guides/ai-agent) — the memory, queue, and streaming patterns in depth.
-- [Authentication](/docs/authentication) — gate which users reach which agent.
-- [BYOC quickstart](/docs/deploy/byoc/quickstart) — stand up the control plane in your own VPC.
diff --git a/src/content/guides/index.mdx b/src/content/guides/index.mdx
index 1a0846bc..c37fe942 100644
--- a/src/content/guides/index.mdx
+++ b/src/content/guides/index.mdx
@@ -1,6 +1,6 @@
---
title: "Guides"
-description: "End-to-end guides for building with Rivet Actors."
+description: "End-to-end guides for building with Rivet."
---
diff --git a/src/metadata/docs-index.ts b/src/metadata/docs-index.ts
index 8b6b25a1..78b95961 100644
--- a/src/metadata/docs-index.ts
+++ b/src/metadata/docs-index.ts
@@ -19,7 +19,7 @@ import { docsRoot } from "../sitemap/docs-sources.node.ts";
import { getProductMetadata } from "../sitemap/product-metadata";
import { deploySlugForContentId } from "../sitemap/deploy";
import { apiSlugForContentId } from "../sitemap/docs-sources";
-import { GUIDES_ROUTE_PREFIX, guidesSlugForContentId, SITE_GUIDES } from "../sitemap/guides";
+import { guidesSlugForContentId } from "../sitemap/guides";
import { productIntegrationsSlugForContentId } from "../sitemap/integrations";
import { registryCategorySlug } from "../sitemap/registry";
import { AGENTOS_REGISTRY_CATEGORIES } from "../data/registry-categories";
@@ -31,9 +31,6 @@ const CONTENT_BASE = path.join(PROJECT_ROOT, "src/content/docs");
// Website-owned product overviews, which shadow each bundle's docs root on the
// site (see src/pages/[product]/[tab]/[...slug].astro).
const OVERVIEWS_BASE = path.join(PROJECT_ROOT, "src/content/overviews");
-// Website-owned solution guides, routed onto the Guides tab (see
-// src/pages/guides/[...slug].astro).
-const GUIDES_BASE = path.join(PROJECT_ROOT, "src/content/guides");
export interface DocPage {
/** Product id, i.e. the first slug segment. */
@@ -80,7 +77,7 @@ export function listDocPages(): DocPage[] {
// Most bundles are served at their content id; the Rivet Cloud bundle
// renders inside the Deploy section, the HTTP API bundle under
- // `/docs/api`, and the Actors `learn` section as the Guides tab instead.
+ // `/docs/api`, and every bundle's `guides` section as the Guides tab instead.
const contentId = normalizeSlug(file.replace(/\.mdx$/, ""));
// Shared bundles (`bundleOf`) are walked twice; only the
// product that routes them gets a Markdown mirror and search entries.
@@ -156,21 +153,6 @@ export function listDocPages(): DocPage[] {
});
}
- for (const guide of SITE_GUIDES) {
- const sourcePath = path.join(GUIDES_BASE, `${guide.slug}.mdx`);
- const raw = readFileSync(sourcePath, "utf-8");
- const { frontmatter, body } = splitFrontmatter(raw);
- pages.push({
- product: "actors",
- slug: normalizeSlug(`${GUIDES_ROUTE_PREFIX.slice(1)}/${guide.slug}`),
- title: frontmatterValue(frontmatter, "title") ?? guide.title,
- description: frontmatterValue(frontmatter, "description") ?? "",
- sourcePath,
- body,
- snippetFiles: listSnippetFiles(body),
- });
- }
-
cache = pages;
return pages;
}
diff --git a/src/metadata/shared.ts b/src/metadata/shared.ts
index b44c2851..b685ac77 100644
--- a/src/metadata/shared.ts
+++ b/src/metadata/shared.ts
@@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url";
import { deploySlugForContentId } from "../sitemap/deploy";
import { apiSlugForContentId } from "../sitemap/docs-sources";
-import { guidesSlugForContentId } from "../sitemap/guides";
+import { guidesSlugForContentId, isGuidesContentId } from "../sitemap/guides";
import {
integrationsSlugForContentId,
productIntegrationsSlugForContentId,
@@ -16,9 +16,9 @@ export const PROJECT_ROOT = fileURLToPath(new URL("../..", import.meta.url));
// Docs slugs are product-scoped (`actors/docs/state`, `agentos/tutorials`),
// so the collection slug is already the site path. The exceptions are the
// Rivet Cloud bundle, which renders inside the Deploy section, the HTTP API
-// bundle, which renders under `/docs/api`, the Actors `learn` and
-// `integrations` sections, which render as the site's Guides and Integrations
-// sections, and any other product's `integrations` section, which renders
+// bundle, which renders under `/docs/api`, every bundle's `guides` section,
+// which renders as the site's Guides tab, the Actors `integrations` section,
+// which renders as the site's Integrations section, and any other product's `integrations` section, which renders
// inside that product's docs (`/agentos/docs/integrations/`).
export function getDocsPath(slug: string) {
const rerooted =
@@ -61,6 +61,8 @@ const UNROUTED_DOCS_PREFIXES = PRODUCTS.flatMap((product) => [
*/
export function isRoutedDocsContentId(contentId: string) {
const slug = normalizeSlug(contentId);
+ // A bundle's own `guides/index.mdx` is replaced by the website's overview.
+ if (isGuidesContentId(slug)) return guidesSlugForContentId(slug) !== undefined;
return !UNROUTED_DOCS_PREFIXES.some(
(prefix) => slug === prefix || slug.startsWith(`${prefix}/`),
);
diff --git a/src/pages/guides/[...slug].astro b/src/pages/guides/[...slug].astro
index 461c9451..dbb8d183 100644
--- a/src/pages/guides/[...slug].astro
+++ b/src/pages/guides/[...slug].astro
@@ -1,70 +1,58 @@
---
// The Guides tab: /guides/{...slug}
//
-// src/content/docs/actors/learn/** the Actors bundle's guides, re-rooted here
-// src/content/guides/*.mdx website-owned: the overview (`index`) and
-// the solution guides (SITE_GUIDES)
+// src/content/docs//guides/** each product bundle's guides
+// src/content/guides/index.mdx website-owned overview
//
// All share the sidebar built by `guidesSidebar()` in src/sitemap/products.ts.
-// The overview is website-owned rather than the bundle's `learn/index.mdx`
-// because it is generated from that sidebar (`GuideCards`).
+// The overview is website-owned because it is generated from that sidebar
+// (`GuideCards`).
import { getCollection } from 'astro:content';
import { ogImageFor } from '@/lib/ogImage';
import DocsArticlePage from '@/components/docs/DocsArticlePage.astro';
import { createGuideCards } from '@/components/docs/GuideCards';
import { getContentParamSlug } from '@/lib/content-path';
import { getRouteSeoPolicy, robotsDirective } from '@/lib/routeSeoPolicy';
-import {
- ACTORS_LEARN_CONTENT_PREFIX,
- GUIDES_ROUTE_PREFIX,
- SITE_GUIDES,
- guideHref,
-} from '@/sitemap/guides';
+import { DOCS_SOURCES } from '@/sitemap/docs-sources';
+import { GUIDES_ROUTE_PREFIX, GUIDES_SECTION, guideForContentId, guideHref } from '@/sitemap/guides';
export async function getStaticPaths() {
// `getStaticPaths` is hoisted, so anything it uses must be declared inside.
const GUIDES_OVERVIEW_ID = 'index';
const paths = [];
- const siteGuides = new Map((await getCollection('guides')).map((entry) => [entry.id, entry]));
// Every guide's description, keyed by its site href, for the overview cards.
const descriptions: Record = {};
+ // Guides from different bundles share one flat namespace.
+ const owners = new Map();
- const learnPrefix = `${ACTORS_LEARN_CONTENT_PREFIX}/`;
for (const entry of await getCollection('docs')) {
- // The glob loader collapses `learn/index.mdx` to the id `actors/learn`;
- // that page is replaced by the website-owned overview below.
- if (entry.id === ACTORS_LEARN_CONTENT_PREFIX) continue;
- if (!entry.id.startsWith(learnPrefix)) continue;
- const slug = entry.id.slice(learnPrefix.length);
- descriptions[guideHref(slug)] = entry.data.description;
- paths.push({
- params: { slug: getContentParamSlug(slug) },
- props: {
- entry,
- editUrlOverride: `https://github.com/rivet-dev/rivet/edit/main/docs/actors/content/learn/${slug}.mdx`,
- },
- });
- }
-
- for (const guide of SITE_GUIDES) {
- const entry = siteGuides.get(guide.slug);
- if (!entry) {
+ const guide = guideForContentId(entry.id);
+ if (!guide) continue;
+ const owner = owners.get(guide.slug);
+ if (owner) {
throw new Error(
- `src/sitemap/guides.ts lists "${guide.slug}" but src/content/guides/${guide.slug}.mdx is missing`,
+ `/guides/${guide.slug}/ is defined by both the ${owner} and ${guide.bundle} bundles; rename one`,
);
}
+ owners.set(guide.slug, guide.bundle);
descriptions[guideHref(guide.slug)] = entry.data.description;
+
+ const source = DOCS_SOURCES[guide.bundle];
+ const contentPath = (entry.filePath ?? '').split(`/${guide.bundle}/${GUIDES_SECTION}/`).pop();
paths.push({
- params: { slug: guide.slug },
+ params: { slug: getContentParamSlug(guide.slug) },
props: {
entry,
- editUrlOverride: `https://github.com/rivet-dev/website/edit/main/src/content/guides/${guide.slug}.mdx`,
+ entryIdPrefix: `${guide.bundle}/${GUIDES_SECTION}/`,
+ editUrlOverride: source.localBundle
+ ? `https://github.com/rivet-dev/website/edit/main/${source.localBundle}/docs/content/${GUIDES_SECTION}/${contentPath}`
+ : `https://github.com/rivet-dev/${source.repo}/edit/main/${source.bundlePath ?? 'docs'}/content/${GUIDES_SECTION}/${contentPath}`,
},
});
}
- const overview = siteGuides.get(GUIDES_OVERVIEW_ID);
+ const overview = (await getCollection('guides')).find((entry) => entry.id === GUIDES_OVERVIEW_ID);
if (!overview) {
throw new Error(`The Guides overview src/content/guides/${GUIDES_OVERVIEW_ID}.mdx is missing`);
}
@@ -72,6 +60,7 @@ export async function getStaticPaths() {
params: { slug: undefined },
props: {
entry: overview,
+ entryIdPrefix: '',
routeSlugOverride: '',
editUrlOverride: `https://github.com/rivet-dev/website/edit/main/src/content/guides/${GUIDES_OVERVIEW_ID}.mdx`,
descriptions,
@@ -81,14 +70,14 @@ export async function getStaticPaths() {
return paths;
}
-const { entry, routeSlugOverride, editUrlOverride, descriptions } = Astro.props;
+const { entry, entryIdPrefix, routeSlugOverride, editUrlOverride, descriptions } = Astro.props;
const robots = robotsDirective(getRouteSeoPolicy(Astro.url));
---
` and its sidebar links `/actors/learn/`; both
- * are re-rooted here so the bundle needs no change.
- * - Website-owned solution guides in `src/content/guides/.mdx`, listed
- * in `SITE_GUIDES` because the Solutions menu links to them.
+ * //content/guides/.mdx -> /guides//
+ * //sidebar.json "guides" -> groups of the Guides sidebar
*
- * Both render through `src/pages/guides/[...slug].astro`.
+ * A bundle links its guides as `/guides/`, where they render, so nothing
+ * is re-rooted. The website owns only the overview (`src/content/guides/index.mdx`).
+ * Guides render through `src/pages/guides/[...slug].astro`, and their
+ * `` paths resolve against the bundle's own repo.
*/
export const GUIDES_ROUTE_PREFIX = "/guides";
-/** Content-collection prefix of the Actors bundle's guides. */
-export const ACTORS_LEARN_CONTENT_PREFIX = "actors/learn";
+/** The bundle directory and `sidebar.json` key that hold a bundle's guides. */
+export const GUIDES_SECTION = "guides";
-/** The bundle's own href prefix for those pages. */
-const ACTORS_LEARN_HREF_PREFIX = "/actors/learn";
-
-export interface SiteGuide {
- slug: string;
- title: string;
- /** Sidebar group. Defaults to Solutions. */
- group?: string;
-}
-
-export const SITE_GUIDES: SiteGuide[] = [
- { slug: "coding-agents", title: "Coding Agents" },
- { slug: "agent-app-builders", title: "Agent App Builders" },
- { slug: "company-agents", title: "Company-Specific Agents" },
- {
- // Prose rather than a worked example, so it sits in its own group rather
- // than beside the solution guides.
- slug: "a-radically-simpler-architecture",
- title: "A Radically Simpler Architecture",
- group: "Architecture",
- },
-];
+/**
+ * Bundles that may ship guides, in the order the Guides sidebar merges them.
+ * Shared bundles (`bundleOf`) are read once, through their source product.
+ */
+export const GUIDE_BUNDLES: string[] = PRODUCTS.filter(
+ (product) => ownsDocsBundle(product) && !product.bundleOf && !product.unlaunched,
+).map((product) => product.id);
export function guideHref(slug: string): string {
return slug ? `${GUIDES_ROUTE_PREFIX}/${slug}/` : `${GUIDES_ROUTE_PREFIX}/`;
}
/**
- * Site slug (no leading slash) of an Actors `learn` content id, e.g.
- * `actors/learn/chat-room` -> `guides/chat-room`, `actors/learn` -> `guides`.
- * Undefined for ids outside that section.
+ * The bundle and guide slug of a docs content id, e.g.
+ * `agents/guides/sign-in-with-chatgpt` -> `{ bundle: "agents", slug: "sign-in-with-chatgpt" }`.
+ * Undefined for ids outside a bundle's guides, and for a bundle's own
+ * `guides/index.mdx`, which the website overview replaces.
*/
-export function guidesSlugForContentId(contentId: string): string | undefined {
- if (contentId === ACTORS_LEARN_CONTENT_PREFIX)
- return GUIDES_ROUTE_PREFIX.slice(1);
- if (!contentId.startsWith(`${ACTORS_LEARN_CONTENT_PREFIX}/`))
- return undefined;
- return `${GUIDES_ROUTE_PREFIX.slice(1)}/${contentId.slice(ACTORS_LEARN_CONTENT_PREFIX.length + 1)}`;
+export function guideForContentId(
+ contentId: string,
+): { bundle: string; slug: string } | undefined {
+ for (const bundle of GUIDE_BUNDLES) {
+ const prefix = `${bundle}/${GUIDES_SECTION}/`;
+ if (!contentId.startsWith(prefix)) continue;
+ const slug = contentId.slice(prefix.length).replace(/\/index$/, "");
+ return slug && slug !== "index" ? { bundle, slug } : undefined;
+ }
+ return undefined;
}
-/** Re-roots the bundle's `/actors/learn/...` hrefs onto `/guides/...`. */
-export function rerootLearnHref(href: string): string {
- if (
- href === ACTORS_LEARN_HREF_PREFIX ||
- href === `${ACTORS_LEARN_HREF_PREFIX}/`
- ) {
- return `${GUIDES_ROUTE_PREFIX}/`;
- }
- if (href.startsWith(`${ACTORS_LEARN_HREF_PREFIX}/`)) {
- return `${GUIDES_ROUTE_PREFIX}/${href.slice(ACTORS_LEARN_HREF_PREFIX.length + 1)}`;
- }
- return href;
+/**
+ * Site slug (no leading slash) of a guide's content id, e.g.
+ * `actors/guides/chat-room` -> `guides/chat-room`. Undefined for other ids.
+ */
+export function guidesSlugForContentId(contentId: string): string | undefined {
+ const guide = guideForContentId(contentId);
+ return guide && `${GUIDES_ROUTE_PREFIX.slice(1)}/${guide.slug}`;
}
-/** The sidebar groups that list every website-owned guide, in first-seen order. */
-export const SITE_GUIDES_SIDEBAR_GROUPS: SidebarItem[] = SITE_GUIDES.reduce(
- (groups: SidebarItem[], guide) => {
- const title = guide.group ?? "Solutions";
- const page = { title: guide.title, href: guideHref(guide.slug) };
- const group = groups.find((candidate) => candidate.title === title);
- if (group) group.pages.push(page);
- else groups.push({ title, pages: [page] });
- return groups;
- },
- [],
-);
+/** Whether a docs content id sits in a bundle's guides section. */
+export function isGuidesContentId(contentId: string): boolean {
+ return GUIDE_BUNDLES.some(
+ (bundle) =>
+ contentId === `${bundle}/${GUIDES_SECTION}` ||
+ contentId.startsWith(`${bundle}/${GUIDES_SECTION}/`),
+ );
+}
diff --git a/src/sitemap/integrations.ts b/src/sitemap/integrations.ts
index ae5bba4d..ac1d3af8 100644
--- a/src/sitemap/integrations.ts
+++ b/src/sitemap/integrations.ts
@@ -1,8 +1,7 @@
/**
* The Integrations section (`/integrations/`): third-party frameworks and SDKs
* backed by Rivet Actors. Its pages are authored in the Actors bundle under
- * `actors/integrations/` and re-rooted here, the same way the bundle's
- * `learn` section renders as `/guides/`. Any other product (agentOS) renders
+ * `actors/integrations/` and re-rooted here. Any other product (agentOS) renders
* its integrations inside its Documentation tab at
* `//docs/integrations/`, reached from a fold in the docs sidebar.
*
diff --git a/src/sitemap/products.ts b/src/sitemap/products.ts
index 1449bedc..9cf28de7 100644
--- a/src/sitemap/products.ts
+++ b/src/sitemap/products.ts
@@ -5,8 +5,9 @@ import {
faPuzzlePiece,
faRobot,
faSparkles,
+ faSquareInfo,
} from "@rivet-gg/icons";
-import type { SidebarItem } from "@/lib/sitemap";
+import type { SidebarItem, SidebarSection } from "@/lib/sitemap";
import rawSidebars from "@/generated/sidebars.json";
import { SIDEBAR_ICONS } from "@/generated/sidebar-icons";
import {
@@ -22,7 +23,7 @@ import {
} from "./self-host";
import { API_DOCS_NAMESPACE, SITE_DOCS_NAMESPACE } from "./docs-sources";
import { CLOUD_BUNDLE_ID } from "./deploy";
-import { rerootLearnHref, SITE_GUIDES_SIDEBAR_GROUPS } from "./guides";
+import { GUIDE_BUNDLES, guideHref } from "./guides";
import { integrationFold, integrationSidebar } from "@/data/integrations";
import { registryFold } from "@/data/registry";
import { integrationsHref, SITE_INTEGRATIONS_PRODUCT } from "./integrations";
@@ -54,12 +55,14 @@ function hydrateIcons(node: T): T {
return out as T;
}
+// Icons are export names in the JSON and become `IconDefinition`s here, so the
+// raw JSON is not yet the hydrated shape.
const SIDEBARS = hydrateIcons(
- rawSidebars as Record<
+ rawSidebars as unknown as Record<
string,
{
docs: SidebarItem[];
- learn?: SidebarItem[];
+ guides?: SidebarItem[];
tutorials?: SidebarItem[];
integrations?: SidebarItem[];
byoc?: SidebarItem[];
@@ -144,26 +147,29 @@ export function deploySidebar(): SidebarItem[] {
}
/**
- * The Guides tab's sidebar: the Actors bundle's `learn` section re-rooted at
- * `/guides/`, followed by the website-owned solution guides.
+ * The Guides tab's sidebar: the website's overview, then every bundle's
+ * `guides` groups in product order. Groups with the same title merge into one,
+ * at the position the title first appears, keeping each bundle's page order.
*/
export function guidesSidebar(): SidebarItem[] {
- const actors = bundleSidebars("actors");
- const learn = actors.learn ?? actors.tutorials ?? [];
- return [...rerootHrefs(learn), ...SITE_GUIDES_SIDEBAR_GROUPS];
-}
-
-function rerootHrefs(node: T): T {
- if (Array.isArray(node)) return node.map(rerootHrefs) as unknown as T;
- if (!node || typeof node !== "object") return node;
- const out: Record = {};
- for (const [key, value] of Object.entries(node as Record)) {
- out[key] =
- key === "href" && typeof value === "string"
- ? rerootLearnHref(value)
- : rerootHrefs(value);
+ const overview: SidebarSection = {
+ title: "General",
+ pages: [{ title: "Overview", href: guideHref(""), icon: faSquareInfo }],
+ };
+ const merged: SidebarSection[] = [];
+ for (const bundleId of GUIDE_BUNDLES) {
+ for (const item of SIDEBARS[bundleId]?.guides ?? []) {
+ if (!("pages" in item)) {
+ throw new Error(
+ `${bundleId}/sidebar.json: every "guides" entry must be a group with "title" and "pages"`,
+ );
+ }
+ const group = merged.find((candidate) => candidate.title === item.title);
+ if (group) group.pages.push(...item.pages);
+ else merged.push({ ...item, pages: [...item.pages] });
+ }
}
- return out as T;
+ return [overview, ...merged];
}
export type ProductTabId =