Rework docs navigation and move alternatives to top-level pages (#1487)

* Rework docs navigation and move alternatives to top-level pages

- Add nested docs nav with categories, collapsible groups, breadcrumbs,
  and a right-hand page outline with heading slugs.
- Move alternatives out of /docs/alternatives into /alternatives/*
  marketing pages with 301 redirects.
- Add github-slugger for heading IDs and require explicit width on
  SiteShell so new pages can't accidentally pick the narrow prose layout.
- Remove the best-practices doc from public-docs.

* Make docs content column prose-width
This commit is contained in:
Mohamed Boudra
2026-06-12 12:53:30 +00:00
committed by GitHub
parent fb0c5b27f3
commit 7e46f6a647
54 changed files with 934 additions and 200 deletions
+1
View File
@@ -16,6 +16,7 @@
"@tanstack/react-router": "^1.166.4",
"@tanstack/react-start": "^1.166.4",
"framer-motion": "^12.35.2",
"github-slugger": "^2.0.0",
"lucide-react": "^1.7.0",
"react": "^19.1.4",
"react-dom": "^19.1.4",
@@ -0,0 +1,38 @@
import { DocsMarkdown } from "~/components/docs-markdown";
import { SiteShell } from "~/components/site-shell";
import { getAlternativePage } from "~/data/alternative-pages";
import { pageMeta } from "~/meta";
export function alternativeRouteOptions(slug: string) {
const page = getAlternativePage(slug);
return {
head: () =>
pageMeta(
page?.title ?? "Alternative - Paseo",
page?.description ?? "",
`/alternatives/${slug}`,
),
component: function AlternativePageRoute() {
return <AlternativePageContent slug={slug} />;
},
};
}
function AlternativePageContent({ slug }: { slug: string }) {
const page = getAlternativePage(slug);
if (!page) {
return (
<SiteShell width="default">
<p className="text-muted-foreground">Page not found.</p>
</SiteShell>
);
}
return (
<SiteShell width="default">
<DocsMarkdown>{page.content}</DocsMarkdown>
</SiteShell>
);
}
@@ -0,0 +1,34 @@
import { Link } from "@tanstack/react-router";
import { ChevronRight } from "lucide-react";
import { type Doc, type DocsNavNode, getDocBreadcrumbGroups } from "~/docs";
interface DocsBreadcrumbsProps {
doc: Doc;
tree: DocsNavNode[];
}
export function DocsBreadcrumbs({ doc, tree }: DocsBreadcrumbsProps) {
const groups = getDocBreadcrumbGroups(doc, tree);
return (
<nav aria-label="Breadcrumb" className="not-prose mb-6">
<ol className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<li>
<Link to="/docs" className="hover:text-foreground transition-colors">
Docs
</Link>
</li>
{groups.map((group) => (
<li key={group.label} className="flex items-center gap-2">
<ChevronRight size={14} className="text-border" />
<span>{group.label}</span>
</li>
))}
<li className="flex items-center gap-2">
<ChevronRight size={14} className="text-border" />
<span className="text-foreground">{doc.frontmatter.nav}</span>
</li>
</ol>
</nav>
);
}
@@ -70,12 +70,14 @@ const docsMarkdownComponents: Components = {
export function DocsMarkdown({ children }: { children: string }) {
return (
<ReactMarkdown
remarkPlugins={docsRemarkPlugins}
rehypePlugins={docsRehypePlugins}
components={docsMarkdownComponents}
>
{children}
</ReactMarkdown>
<div className="docs-prose">
<ReactMarkdown
remarkPlugins={docsRemarkPlugins}
rehypePlugins={docsRehypePlugins}
components={docsMarkdownComponents}
>
{children}
</ReactMarkdown>
</div>
);
}
@@ -0,0 +1,152 @@
import { Link, useLocation } from "@tanstack/react-router";
import { ChevronDown, ChevronRight } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { type DocsNavNode } from "~/docs";
interface DocsNavProps {
nodes: DocsNavNode[];
mobile?: boolean;
onNavigate?: () => void;
}
const ACTIVE_OPTIONS_EXACT = { exact: true };
function nodeContainsHref(node: DocsNavNode, href: string): boolean {
if (node.type === "page") return node.href === href;
return node.children.some((child) => nodeContainsHref(child, href));
}
function clsx(...classes: (string | false | null | undefined)[]): string {
return classes.filter(Boolean).join(" ");
}
function PageLink({
node,
mobile,
onNavigate,
}: {
node: Extract<DocsNavNode, { type: "page" }>;
mobile?: boolean;
onNavigate?: () => void;
}) {
const location = useLocation();
const isActive = location.pathname === node.href;
return (
<Link
to={node.href}
activeOptions={ACTIVE_OPTIONS_EXACT}
onClick={onNavigate}
className={clsx(
"block px-3 py-2 text-sm rounded-md transition-colors",
mobile
? "text-muted-foreground hover:text-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-muted",
isActive && (mobile ? "text-foreground" : "bg-muted text-foreground"),
)}
>
{node.label}
</Link>
);
}
function GroupNode({
node,
mobile,
onNavigate,
}: {
node: Extract<DocsNavNode, { type: "group" }>;
mobile?: boolean;
onNavigate?: () => void;
}) {
const location = useLocation();
const currentHref = location.pathname;
const containsActive = useMemo(() => nodeContainsHref(node, currentHref), [node, currentHref]);
const [isOpen, setIsOpen] = useState(containsActive);
const toggle = useCallback(() => setIsOpen((open) => !open), []);
useEffect(() => {
setIsOpen(containsActive);
}, [containsActive]);
return (
<div>
<button
type="button"
onClick={toggle}
className={clsx(
"w-full flex items-center justify-between gap-2 px-3 py-2 text-sm rounded-md transition-colors",
mobile
? "text-muted-foreground hover:text-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-muted",
containsActive && "text-foreground",
)}
>
<span>{node.label}</span>
{isOpen ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</button>
{isOpen && (
<div className="ml-3 pl-3 border-l border-border space-y-0.5">
<NavTree nodes={node.children} mobile={mobile} onNavigate={onNavigate} />
</div>
)}
</div>
);
}
function CategoryNode({
node,
mobile,
onNavigate,
}: {
node: Extract<DocsNavNode, { type: "category" }>;
mobile?: boolean;
onNavigate?: () => void;
}) {
return (
<div className={mobile ? "space-y-1" : "space-y-1 mt-6 first:mt-0"}>
<div className="px-3 py-2 text-xs font-medium text-foreground">{node.label}</div>
<NavTree nodes={node.children} mobile={mobile} onNavigate={onNavigate} />
</div>
);
}
function NavTree({ nodes, mobile, onNavigate }: DocsNavProps) {
return (
<div className="space-y-0.5">
{nodes.map((node) => {
if (node.type === "category") {
return (
<CategoryNode
key={`category-${node.label}`}
node={node}
mobile={mobile}
onNavigate={onNavigate}
/>
);
}
if (node.type === "group") {
return (
<GroupNode
key={`group-${node.segment}`}
node={node}
mobile={mobile}
onNavigate={onNavigate}
/>
);
}
return (
<PageLink key={`page-${node.href}`} node={node} mobile={mobile} onNavigate={onNavigate} />
);
})}
</div>
);
}
export function DocsNav({ nodes, mobile, onNavigate }: DocsNavProps) {
return (
<div className={mobile ? undefined : "-ml-3"}>
<NavTree nodes={nodes} mobile={mobile} onNavigate={onNavigate} />
</div>
);
}
@@ -0,0 +1,109 @@
import { useCallback, useEffect, useState } from "react";
import { type DocHeading } from "~/docs";
interface DocsOutlineProps {
headings: DocHeading[];
}
function clsx(...classes: (string | false | null | undefined)[]): string {
return classes.filter(Boolean).join(" ");
}
function OutlineLink({
heading,
active,
onActivate,
}: {
heading: DocHeading;
active: boolean;
onActivate: (id: string) => void;
}) {
const handleClick = useCallback(
(event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
const element = document.getElementById(heading.id);
if (element) {
element.scrollIntoView({ behavior: "smooth" });
window.history.replaceState(null, "", `#${heading.id}`);
onActivate(heading.id);
}
},
[heading.id, onActivate],
);
return (
<a
href={`#${heading.id}`}
onClick={handleClick}
className={clsx(
"block py-1 transition-colors border-l-2 -ml-px",
heading.depth === 2 && "pl-3",
heading.depth === 3 && "pl-6",
heading.depth >= 4 && "pl-9",
active
? "text-foreground border-primary"
: "text-muted-foreground hover:text-foreground border-transparent",
)}
>
{heading.text}
</a>
);
}
export function DocsOutline({ headings }: DocsOutlineProps) {
const [activeId, setActiveId] = useState<string | null>(() =>
headings.length > 0 ? headings[0].id : null,
);
useEffect(() => {
setActiveId(headings.length > 0 ? headings[0].id : null);
}, [headings]);
useEffect(() => {
if (headings.length === 0) return;
const observer = new IntersectionObserver(
(entries) => {
const visible = entries
.filter((entry) => entry.isIntersecting)
.map((entry) => entry.target.id);
if (visible.length > 0) {
setActiveId(visible[0]);
}
},
{
rootMargin: "-10% 0px -70% 0px",
threshold: 0,
},
);
for (const heading of headings) {
const element = document.getElementById(heading.id);
if (element) observer.observe(element);
}
return () => observer.disconnect();
}, [headings]);
if (headings.length === 0) return null;
return (
<nav
aria-label="On this page"
className="sticky top-8 max-h-[calc(100vh-4rem)] overflow-y-auto py-8 pr-4"
>
<div className="text-xs font-medium text-muted-foreground mb-3 px-3">On this page</div>
<ul className="space-y-1 border-l border-border">
{headings.map((heading) => (
<li key={heading.id} className="text-sm">
<OutlineLink
heading={heading}
active={activeId === heading.id}
onActivate={setActiveId}
/>
</li>
))}
</ul>
</nav>
);
}
@@ -1,3 +1,4 @@
import { getAlternativePages } from "~/data/alternative-pages";
import { appStoreUrl, playStoreUrl, webAppUrl } from "~/downloads";
interface SiteFooterProps {
@@ -7,9 +8,10 @@ interface SiteFooterProps {
export function SiteFooter({ width = "default" }: SiteFooterProps) {
const widthClasses =
width === "prose" ? "max-w-prose p-6 md:p-12 md:pt-0" : "max-w-5xl p-6 md:p-20 md:pt-0";
const alternatives = getAlternativePages();
return (
<footer className={`${widthClasses} mx-auto`}>
<div className="border-t border-white/10 pt-8 pb-4 grid grid-cols-2 sm:grid-cols-4 gap-8 text-sm">
<div className="border-t border-white/10 pt-8 pb-4 grid grid-cols-2 sm:grid-cols-5 gap-8 text-sm">
<div className="space-y-3">
<p className="text-white/60 font-medium">Product</p>
<div className="space-y-2">
@@ -80,6 +82,20 @@ export function SiteFooter({ width = "default" }: SiteFooterProps) {
</a>
</div>
</div>
<div className="space-y-3">
<p className="text-white/60 font-medium">Alternatives</p>
<div className="space-y-2">
{alternatives.map((page) => (
<a
key={page.slug}
href={page.href}
className="block text-muted-foreground hover:text-foreground transition-colors"
>
{page.name}
</a>
))}
</div>
</div>
<div className="space-y-3">
<p className="text-white/60 font-medium">Community</p>
<div className="space-y-2">
@@ -4,10 +4,10 @@ import { SiteHeader } from "~/components/site-header";
interface SiteShellProps {
children: ReactNode;
width?: "default" | "prose";
width: "default" | "prose";
}
export function SiteShell({ children, width = "default" }: SiteShellProps) {
export function SiteShell({ children, width }: SiteShellProps) {
const mainClasses =
width === "prose" ? "max-w-prose p-6 md:p-12 mx-auto" : "max-w-5xl p-6 md:p-20 mx-auto";
return (
@@ -0,0 +1,118 @@
---
title: Open Source Claude Desktop Alternative With Linux, Mobile, and Multi-Provider Support
description: Paseo is an open source Claude Desktop alternative for developers who want Linux, self-hosting, native mobile apps, and Claude Code alongside Codex, OpenCode, Copilot, and more.
nav: Claude Desktop
order: 55
---
# Paseo vs Claude Desktop
Claude Desktop is Anthropic's desktop app for Claude. It includes Chat, Cowork, and Claude Code in one app. Claude Code runs in the desktop app on macOS and Windows.
Paseo is an app for orchestrating coding agents, with native clients on desktop, mobile, web, and the CLI. Open source (AGPL-3.0).
![Paseo desktop and mobile app](/hero-mockup.png)
## When to pick what
Pick Claude Desktop if you want Anthropic's first-party app for Claude, Claude Cowork, and Claude Code, with Anthropic-managed cloud sessions and the tightest Claude account integration.
Pick Paseo if you want:
- Linux alongside macOS and Windows
- A native iOS and Android app for the same agent workflow
- Claude Code, Codex, OpenCode, Copilot, Pi, and 30+ more agents in one interface
- A self-hosted daemon you can run on a laptop, VM, or dev server
- A CLI and MCP server for scripting and multi-agent workflows
- Open source you can audit and fork
## Architecture
Paseo runs a daemon on your machine. Desktop, web, mobile, and CLI clients connect to it over a websocket. The daemon launches Claude Code and other providers as local processes, using your installed CLIs, credentials, MCP servers, skills, and project config.
Claude Desktop is the host app. The Code tab can run Claude Code locally, connect over SSH, or run remote sessions on Anthropic infrastructure.
## Providers
Claude Desktop runs Claude Code.
Paseo runs Claude Code too, plus Codex, OpenCode, and Pi natively, plus 30+ more agents through the in-app catalog including GitHub Copilot, Cursor, Gemini CLI, and Amp. Paseo speaks the [Agent Client Protocol](https://agentclientprotocol.com), so any ACP agent works. Custom providers run any CLI agent. See [Supported providers](/docs/supported-providers).
## Desktop platforms
Claude Desktop is available on macOS and Windows. Anthropic lists Linux as not available.
Paseo ships on macOS, Linux, and Windows.
## Mobile
Paseo ships native iOS and Android apps with the same agent workflow as the desktop app.
Claude has iOS and Android apps. Claude Code can be controlled from mobile through Remote Control, and Claude Desktop can pair with mobile for some workflows.
## Panes
Both tools support visual coding workflows around Claude Code.
Paseo's app has split panes and tabs (⌘D for vertical, ⌘⇧D for horizontal). Panes include agents, terminals, a diff viewer, and a browser for testing running services.
Claude Desktop has a graphical Code tab with sessions, integrated terminal, file editor, visual diff review, live app preview, PR monitoring, and scheduled tasks.
## GitHub
Paseo's app handles commit, push, opening PRs, watching checks and reviews, and merging.
Claude Desktop can monitor pull request status and can fix failures or merge when checks pass, depending on the workflow and permissions.
## CLI and automation
Claude Code has its own CLI, IDE integrations, web surface, scheduled tasks, and cloud sessions.
Paseo's CLI controls the same daemon as the app:
```bash
paseo run --provider claude "implement OAuth"
paseo run --provider codex --worktree refactor-auth "refactor auth"
paseo run --host devbox:6767 "run the test suite"
paseo ls
paseo send <agent-id> "add tests"
paseo schedule create --cron "0 9 * * 1" "audit the codebase"
```
`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. `paseo loop` retries an agent until a verification command passes. The MCP server lets other agents create worktrees, launch agents, open terminals, and send prompts.
## Worktrees and services
Both tools support parallel coding sessions, including Git worktrees.
Paseo also gives each worktree its own dev server URL. Two agents running their dev servers at the same time get `web.fix-auth.my-app.localhost` and `web.add-search.my-app.localhost` instead of port collisions.
## Voice
Paseo supports dictation and realtime voice mode. Speech-to-text and text-to-speech can run locally on your device.
Claude supports voice in Claude's own mobile and app surfaces. Claude Code itself is available in Claude Desktop, terminal, IDE, web, and mobile Remote Control workflows.
## Comparison
| | Paseo | Claude Desktop |
| ---------------------------- | --------------------------------------------------------------- | --------------------------------- |
| License | Open source (AGPL-3.0) | Not published as open source |
| Desktop platforms | macOS, Linux, Windows | macOS, Windows |
| Native mobile | iOS, Android | iOS, Android Claude apps |
| Coding agents | Claude Code, Codex, OpenCode, Pi + 30+ via ACP catalog + custom | Claude Code |
| General chat | No | Claude Chat |
| Cloud agent | Cloud waitlist | Claude Cowork and remote sessions |
| Local execution | Yes | Yes |
| SSH remote execution | Via daemon on the remote host | Yes |
| Git worktrees | Yes | Yes |
| Per-worktree dev server URLs | Yes | No |
| Split panes and tabs | Yes | Yes |
| In-app terminal | Yes | Yes |
| In-app browser / preview | Yes | Yes |
| GitHub workflow in app | Commit, push, PR, checks, reviews, merge | PR monitoring and merge workflows |
| CLI | Run, `--host`, ls, send, schedule, loop | Claude Code CLI |
| MCP server for orchestration | Yes | MCP support inside Claude Code |
| Self-hosted daemon | Yes | No |
See also: [Paseo vs Codex App](/alternatives/codex-app), [Paseo vs OpenCode Desktop](/alternatives/opencode-desktop), [Paseo vs Conductor](/alternatives/conductor).
@@ -0,0 +1,110 @@
---
title: Open Source Codex App Alternative With Linux, Mobile, and Multi-Provider Support
description: Paseo is an open source alternative to Codex App for developers who want Linux, native mobile apps, a self-hosted daemon, and Codex alongside Claude Code, OpenCode, Copilot, and more.
nav: Codex App
order: 54
---
# Paseo vs Codex App
Codex App is OpenAI's desktop app for working with Codex threads in parallel. It runs on macOS and Windows, with local, worktree, and cloud modes.
Paseo is an app for orchestrating coding agents, with native clients on desktop, mobile, web, and the CLI. Open source (AGPL-3.0).
![Paseo desktop and mobile app](/hero-mockup.png)
## When to pick what
Pick Codex App if you want OpenAI's first-party app for Codex, with Codex-specific features like cloud threads, appshots, image generation, and computer use on macOS.
Pick Paseo if you want:
- Linux alongside macOS and Windows
- A native iOS and Android app
- Codex, Claude Code, OpenCode, Copilot, Pi, and 30+ more agents in one interface
- A self-hosted daemon you can run on a laptop, VM, or dev server
- A CLI and MCP server for scripting and multi-agent workflows
- Open source you can audit and fork
## Architecture
Paseo runs a daemon on your machine. Desktop, web, mobile, and CLI clients connect to it over a websocket. The daemon launches Codex and other providers as local processes, using your installed CLIs and credentials.
Codex App is a desktop app for Codex. It can run local and worktree threads on your computer, and cloud threads on OpenAI-managed infrastructure.
## Providers
Codex App runs Codex.
Paseo runs Codex too, plus Claude Code, OpenCode, and Pi natively, plus 30+ more agents through the in-app catalog including GitHub Copilot, Cursor, Gemini CLI, and Amp. Paseo speaks the [Agent Client Protocol](https://agentclientprotocol.com), so any ACP agent works. Custom providers run any CLI agent. See [Supported providers](/docs/supported-providers).
## Desktop platforms
Codex App is available on macOS and Windows. OpenAI lists Linux as not available yet.
Paseo ships on macOS, Linux, and Windows.
## Mobile
Paseo ships native iOS and Android apps with the same agent workflow as the desktop app.
Codex can be controlled remotely through OpenAI's mobile surfaces, including ChatGPT mobile remote connections. Codex App itself is a desktop app.
## Worktrees and local setup
Both tools support Git worktrees for parallel work.
Codex App creates Codex-managed worktrees under `$CODEX_HOME/worktrees` and supports local environment setup scripts and project actions through `.codex` configuration.
Paseo creates worktrees under `$PASEO_HOME/worktrees`, runs setup and teardown hooks from `paseo.json`, and gives each worktree its own dev server URLs like `web.fix-auth.my-app.localhost` so parallel services don't fight for ports.
## GitHub and review
Both tools support reviewing diffs, committing, pushing, and opening pull requests from the app.
Paseo also surfaces PR checks and reviews in the app, and exposes the same workflow through the CLI and MCP server.
## CLI and automation
Codex has its own CLI, IDE extension, web app, automations, and SDK.
Paseo's CLI controls the same daemon as the app:
```bash
paseo run --provider codex "implement OAuth"
paseo run --provider claude --worktree refactor-auth "refactor auth"
paseo run --host devbox:6767 "run the test suite"
paseo ls
paseo send <agent-id> "add tests"
paseo schedule create --cron "0 9 * * 1" "audit the codebase"
```
`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. `paseo loop` retries an agent until a verification command passes. The MCP server lets other agents create worktrees, launch agents, open terminals, and send prompts.
## Voice
Codex App supports voice dictation.
Paseo supports dictation and realtime voice mode. Speech-to-text and text-to-speech can run locally on your device.
## Comparison
| | Paseo | Codex App |
| ---------------------------- | --------------------------------------------------------------- | ---------------------------- |
| License | Open source (AGPL-3.0) | Not published as open source |
| Desktop platforms | macOS, Linux, Windows | macOS, Windows |
| Native mobile | iOS, Android | No |
| Providers | Codex, Claude Code, OpenCode, Pi + 30+ via ACP catalog + custom | Codex |
| Local execution | Yes | Yes |
| Cloud execution | Cloud waitlist | Yes |
| Git worktrees | Yes | Yes |
| Per-worktree dev server URLs | Yes | No |
| In-app terminal | Yes | Yes |
| In-app browser | Yes | Yes |
| GitHub workflow in app | Commit, push, PR, checks, reviews, merge | Commit, push, PR |
| CLI | Run, `--host`, ls, send, schedule, loop | Codex CLI |
| MCP server for orchestration | Yes | MCP support inside Codex |
| Voice | Dictation and realtime voice | Dictation |
| Self-hosted daemon | Yes | No |
See also: [Paseo vs Claude Desktop](/alternatives/claude-desktop), [Paseo vs OpenCode Desktop](/alternatives/opencode-desktop), [Supported providers](/docs/supported-providers).
@@ -0,0 +1,97 @@
---
title: Open Source Conductor Alternative With Linux, Windows, and Mobile
description: Paseo is open source, runs on macOS, Linux, and Windows, ships native iOS and Android apps, and supports 30+ agents through the in-app catalog plus any ACP or CLI agent. Conductor is macOS only and Claude Code or Codex only.
nav: Conductor
order: 50
---
# Paseo vs Conductor
Conductor is a macOS app for running Claude Code and Codex in parallel git worktrees. Closed source.
Paseo is an app for orchestrating coding agents, with native clients on desktop, mobile, web, and the CLI. Open source (AGPL-3.0).
![Paseo desktop and mobile app](/hero-mockup.png)
## Why pick Paseo
Conductor runs on macOS, with Claude Code and Codex, in parallel git worktrees. Paseo does all of that. Pick Paseo if you want:
- Linux or Windows alongside macOS
- A native iOS and Android app
- Many more agents than Claude Code and Codex
- A CLI to script agent work and drive remote daemons
- A self-hosted daemon you can run on a server, VM, or homelab
- Open source you can audit and fork
## Architecture
The Paseo daemon runs as its own process. Desktop, web, mobile, and CLI all connect to it over a websocket. Run the daemon on your laptop, on a VM, in Docker, or across a fleet, and connect to any of them from any client.
Conductor's desktop app is the host. Agents run inside it.
## Providers
Paseo runs Claude Code, Codex, OpenCode, and Pi natively, plus 30+ more agents through the in-app catalog including GitHub Copilot, Cursor, Gemini CLI, and Amp. Paseo speaks the [Agent Client Protocol](https://agentclientprotocol.com), so any ACP agent works. Custom providers run any CLI agent. See [Supported providers](/docs/supported-providers).
Conductor runs Claude Code and Codex.
Both tools launch the official CLIs as subprocesses with your own credentials. Neither extracts tokens or proxies model calls.
## Panes
Paseo's app has split panes and tabs (⌘D for vertical, ⌘⇧D for horizontal). Panes include a terminal alongside your agents, a diff viewer, and a browser for testing running services.
## GitHub
Paseo's app handles commit, push, opening PRs, watching checks and reviews, and merging.
## CLI
Paseo has a CLI that mirrors the app:
```bash
paseo run --provider codex "implement OAuth"
paseo run --host devbox:6767 "run the test suite"
paseo ls
paseo send <agent-id> "add tests"
paseo schedule create --cron "0 9 * * 1" "audit the codebase"
```
`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. `paseo loop` retries an agent until a verification command passes.
Conductor does not have a CLI.
## Worktrees and services
Both tools isolate parallel agents in git worktrees.
Paseo also gives each worktree its own dev server URL. Two agents running their dev servers at the same time get `web.fix-auth.my-app.localhost` and `web.add-search.my-app.localhost` instead of port collisions.
## Mobile
Paseo ships native iOS and Android apps with the same feature set as the desktop app. Conductor has no mobile app.
## Voice
Paseo's speech-to-text and text-to-speech run locally on your device. Nothing leaves your network. Conductor does not have voice.
## Comparison
| | Paseo | Conductor |
| ---------------------------- | --------------------------------------------------------------- | ------------------ |
| License | Open source (AGPL-3.0) | Closed source |
| Platforms | macOS, Linux, Windows | macOS only |
| Native mobile | iOS, Android | — |
| Providers | Claude Code, Codex, OpenCode, Pi + 30+ via ACP catalog + custom | Claude Code, Codex |
| Git worktrees | Yes | Yes |
| Per-worktree dev server URLs | Yes | — |
| Split panes and tabs | Yes | — |
| In-app terminal | Yes | Yes |
| In-app browser | Yes | — |
| GitHub workflow in app | Commit, push, PR, checks, reviews, merge | Yes |
| CLI | Run, `--host`, ls, send, schedule, loop | — |
| Local voice (on-device) | Yes | — |
| Self-hosted daemon | Yes | — |
See also: [Paseo vs Superset](/alternatives/superset), [Paseo vs OpenChamber](/alternatives/openchamber), [Paseo vs Happy Coder](/alternatives/happy-coder).
@@ -0,0 +1,99 @@
---
title: Happy Coder Alternative With a Desktop App and Git Worktrees
description: Paseo ships a native desktop app, runs agents in isolated git worktrees, and supports 30+ agents. Happy Coder is mobile and web only, wraps the agent CLI, and supports Claude Code and Codex.
nav: Happy Coder
order: 53
---
# Paseo vs Happy Coder
Happy Coder is a mobile and web client for Claude Code and Codex. It wraps the agent CLI on your laptop and syncs sessions to phone and browser over an end-to-end encrypted relay. Open source under MIT.
Paseo is an app for orchestrating coding agents, with native clients on desktop, mobile, web, and the CLI. Open source (AGPL-3.0).
![Paseo desktop and mobile app](/hero-mockup.png)
## When to pick what
Pick Happy Coder if you want the most minimal setup. Wrap an existing Claude Code or Codex session on your laptop and check in on it from your phone.
Pick Paseo if you want:
- A native desktop app on macOS, Linux, and Windows
- Git worktrees for parallel agents
- Per-worktree dev server URLs
- GitHub PRs, checks, reviews, and merges in the app
- Many more agents than Claude Code and Codex
- A CLI to script agent work and drive remote daemons
## Architecture
Paseo runs the agent inside its own daemon. The daemon owns the agent lifecycle, the worktree, and the dev servers. Clients connect over a websocket and drive the daemon.
Happy Coder runs the agent inside its existing CLI on your laptop and syncs the session to its mobile and web clients through an end-to-end encrypted relay.
## Panes
Paseo's app has split panes and tabs (⌘D for vertical, ⌘⇧D for horizontal). Panes include a terminal alongside your agents, a diff viewer, and a browser for testing running services.
Happy Coder does not have a desktop app.
## GitHub
Paseo's app handles commit, push, opening PRs, watching checks and reviews, and merging.
## Mobile
Both tools ship native iOS and Android apps.
## Providers
Paseo runs Claude Code, Codex, OpenCode, and Pi natively, plus 30+ more agents through the in-app catalog including GitHub Copilot, Cursor, Gemini CLI, and Amp. Paseo speaks the [Agent Client Protocol](https://agentclientprotocol.com), so any ACP agent works. Custom providers run any CLI agent. See [Supported providers](/docs/supported-providers).
Happy Coder runs Claude Code and Codex.
## Worktrees and services
Paseo runs each agent in its own git worktree. Each worktree gets its own dev server URL like `web.fix-auth.my-app.localhost`, so parallel agents don't fight for the same port.
Happy Coder runs the agent in the directory you launched the CLI from.
## CLI
Paseo has a CLI that mirrors the app:
```bash
paseo run --provider codex "implement OAuth"
paseo run --host devbox:6767 "run the test suite"
paseo ls
paseo send <agent-id> "add tests"
paseo schedule create --cron "0 9 * * 1" "audit the codebase"
```
`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. `paseo loop` retries an agent until a verification command passes.
Happy Coder has a CLI to launch the wrapped session. It does not have schedules or loops.
## Voice
Paseo's speech-to-text and text-to-speech run locally on your device. Nothing leaves your network.
## Comparison
| | Paseo | Happy Coder |
| ---------------------------- | --------------------------------------------------------------- | ---------------------- |
| License | Open source (AGPL-3.0) | Open source (MIT) |
| Desktop app | macOS, Linux, Windows | — |
| Native mobile | iOS, Android | iOS, Android |
| Architecture | Daemon owns agent lifecycle | Wraps the agent CLI |
| Providers | Claude Code, Codex, OpenCode, Pi + 30+ via ACP catalog + custom | Claude Code, Codex |
| Split panes and tabs | Yes | — |
| In-app terminal | Yes | — |
| In-app browser | Yes | — |
| GitHub workflow in app | Commit, push, PR, checks, reviews, merge | — |
| Git worktrees | Yes | — |
| Per-worktree dev server URLs | Yes | — |
| CLI | Run, `--host`, ls, send, schedule, loop | Launch wrapped session |
| Local voice (on-device) | Yes | — |
See also: [Paseo vs Conductor](/alternatives/conductor), [Paseo vs Superset](/alternatives/superset), [Paseo vs OpenChamber](/alternatives/openchamber).
@@ -0,0 +1,93 @@
---
title: OpenChamber Alternative With Linux, Windows, and Mobile
description: Paseo ships native iOS and Android apps, runs on macOS, Linux, and Windows, and supports 30+ agents. OpenChamber is macOS only with a PWA and is built around OpenCode.
nav: OpenChamber
order: 52
---
# Paseo vs OpenChamber
OpenChamber is a macOS desktop app for OpenCode. Also available as a PWA. Open source under MIT.
Paseo is an app for orchestrating coding agents, with native clients on desktop, mobile, web, and the CLI. Open source (AGPL-3.0).
![Paseo desktop and mobile app](/hero-mockup.png)
## Why pick Paseo
OpenChamber runs on macOS, around OpenCode, with a phone PWA. Paseo runs OpenCode too, on macOS, and adds:
- Linux and Windows desktop
- A native iOS and Android app
- Many more agents than OpenCode (Claude Code, Codex, Pi, plus 30+ more via the in-app ACP catalog)
- A scriptable CLI to drive agents and connect to remote daemons
## Mobile
Paseo ships a native iOS and Android app with the same feature set as the desktop. Install from the App Store or Google Play.
OpenChamber does not have a native mobile app.
## Desktop
Paseo ships on macOS, Linux, and Windows.
OpenChamber ships on macOS.
## Providers
Paseo runs Claude Code, Codex, OpenCode, and Pi natively, plus 30+ more agents through the in-app catalog including GitHub Copilot, Cursor, Gemini CLI, and Amp. Paseo speaks the [Agent Client Protocol](https://agentclientprotocol.com), so any ACP agent works. Custom providers run any CLI agent. See [Supported providers](/docs/supported-providers).
OpenChamber is built around OpenCode.
## Panes
Paseo's app has split panes and tabs (⌘D for vertical, ⌘⇧D for horizontal). Panes include a terminal alongside your agents, a diff viewer, and a browser for testing running services.
## GitHub
Paseo's app handles commit, push, opening PRs, watching checks and reviews, and merging.
## CLI
Paseo has a CLI that mirrors the app:
```bash
paseo run --provider codex "implement OAuth"
paseo run --host devbox:6767 "run the test suite"
paseo ls
paseo send <agent-id> "add tests"
paseo schedule create --cron "0 9 * * 1" "audit the codebase"
```
`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. `paseo loop` retries an agent until a verification command passes.
OpenChamber does not have a CLI.
## Worktrees and services
Paseo runs each agent in its own git worktree. Each worktree gets its own dev server URL like `web.fix-auth.my-app.localhost`, so parallel agents don't fight for ports.
## Voice
Paseo's speech-to-text and text-to-speech run locally on your device. OpenChamber does not have voice.
## Comparison
| | Paseo | OpenChamber |
| ---------------------------- | --------------------------------------------------------------- | ----------------- |
| License | Open source (AGPL-3.0) | Open source (MIT) |
| Desktop platforms | macOS, Linux, Windows | macOS |
| Mobile | Native iOS, Android | PWA |
| Providers | Claude Code, Codex, OpenCode, Pi + 30+ via ACP catalog + custom | OpenCode |
| Split panes and tabs | Yes | — |
| In-app terminal | Yes | — |
| In-app browser | Yes | — |
| GitHub workflow in app | Commit, push, PR, checks, reviews, merge | Yes |
| CLI | Run, `--host`, ls, send, schedule, loop | — |
| Git worktrees | Yes | Yes |
| Per-worktree dev server URLs | Yes | — |
| Local voice (on-device) | Yes | — |
| Self-hosted daemon | Yes | — |
See also: [Paseo vs Conductor](/alternatives/conductor), [Paseo vs Superset](/alternatives/superset), [Paseo vs Happy Coder](/alternatives/happy-coder).
@@ -0,0 +1,116 @@
---
title: OpenCode Desktop Alternative With Native Mobile and Multi-Provider Orchestration
description: Paseo is an OpenCode Desktop alternative for developers who want native mobile apps, a self-hosted daemon, and OpenCode alongside Claude Code, Codex, Copilot, and more.
nav: OpenCode Desktop
order: 56
---
# Paseo vs OpenCode Desktop
OpenCode Desktop is the desktop app for OpenCode. It is available in beta for macOS, Windows, and Linux.
Paseo is an app for orchestrating coding agents, with native clients on desktop, mobile, web, and the CLI. Open source (AGPL-3.0).
![Paseo desktop and mobile app](/hero-mockup.png)
## When to pick what
Pick OpenCode Desktop if you want the OpenCode app, the OpenCode terminal workflow, and OpenCode's multi-model provider system.
Pick Paseo if you want:
- A native iOS and Android app
- OpenCode beside Claude Code, Codex, Copilot, Pi, and 30+ more agents
- First-party harnesses for Claude Code and Codex instead of one agent runtime for everything
- A self-hosted daemon you can run on a laptop, VM, or dev server
- GitHub PRs, checks, reviews, and merges in the app
- A CLI and MCP server for scripting and multi-agent workflows
## Architecture
Paseo runs a daemon on your machine. Desktop, web, mobile, and CLI clients connect to it over a websocket. The daemon launches OpenCode and other providers as local processes, using your installed CLIs and credentials.
OpenCode Desktop is the desktop app for OpenCode. OpenCode is available as a terminal interface, desktop app, IDE extension, web surface, and integrations.
## Providers
OpenCode is a multi-model coding agent. It can connect to many LLM providers through its own provider system, including OpenCode Zen, local models, and API providers.
Paseo is multi-provider at the agent harness layer. It runs OpenCode, Claude Code, Codex, and Pi natively, plus 30+ more agents through the in-app catalog including GitHub Copilot, Cursor, Gemini CLI, and Amp. Paseo speaks the [Agent Client Protocol](https://agentclientprotocol.com), so any ACP agent works. Custom providers run any CLI agent. See [Supported providers](/docs/supported-providers).
## Desktop platforms
Both tools ship on macOS, Linux, and Windows.
## Mobile
Paseo ships native iOS and Android apps with the same agent workflow as the desktop app.
OpenCode Desktop is a desktop app. OpenCode also has web and share-link workflows, but not a native mobile app.
## Panes
Paseo's app has split panes and tabs (⌘D for vertical, ⌘⇧D for horizontal). Panes include agents, terminals, a diff viewer, and a browser for testing running services.
OpenCode is available in terminal, IDE, and desktop surfaces. Its core workflow centers on OpenCode sessions.
## GitHub
Paseo's app handles commit, push, opening PRs, watching checks and reviews, and merging.
OpenCode has GitHub and GitLab integrations, and OpenCode sessions can make and review code changes through its agent workflow.
## CLI and automation
OpenCode has its own terminal interface, CLI, IDE extension, GitHub and GitLab integrations, and share links.
Paseo's CLI controls the same daemon as the app:
```bash
paseo run --provider opencode "implement OAuth"
paseo run --provider claude --worktree refactor-auth "refactor auth"
paseo run --host devbox:6767 "run the test suite"
paseo ls
paseo send <agent-id> "add tests"
paseo schedule create --cron "0 9 * * 1" "audit the codebase"
```
`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. `paseo loop` retries an agent until a verification command passes. The MCP server lets other agents create worktrees, launch agents, open terminals, and send prompts.
## Worktrees and services
Paseo runs each agent in its own Git worktree. Each worktree gets its own dev server URL like `web.fix-auth.my-app.localhost`, so parallel agents don't fight for ports.
OpenCode supports multi-session work on the same project. If you want worktree isolation around OpenCode sessions, Paseo can provide that by launching OpenCode inside Paseo workspaces.
## Privacy and source
Both tools are open source.
Paseo is AGPL-3.0 and runs your agents through a daemon you control. OpenCode is open source and says it does not store your code or context data by default. OpenCode share links are public when you create them.
## Voice
Paseo supports dictation and realtime voice mode. Speech-to-text and text-to-speech can run locally on your device.
## Comparison
| | Paseo | OpenCode Desktop |
| ---------------------------- | --------------------------------------------------------------- | ------------------------------- |
| License | Open source (AGPL-3.0) | Open source |
| Desktop platforms | macOS, Linux, Windows | macOS, Linux, Windows |
| Native mobile | iOS, Android | No |
| Agent harnesses | Claude Code, Codex, OpenCode, Pi + 30+ via ACP catalog + custom | OpenCode |
| Multi-model support | Through supported agent harnesses | Through OpenCode providers |
| Git worktrees | Yes | No built-in worktree manager |
| Per-worktree dev server URLs | Yes | No |
| Split panes and tabs | Yes | Desktop sessions |
| In-app terminal | Yes | OpenCode terminal workflow |
| In-app browser | Yes | No |
| GitHub workflow in app | Commit, push, PR, checks, reviews, merge | GitHub integration |
| CLI | Run, `--host`, ls, send, schedule, loop | OpenCode CLI |
| MCP server for orchestration | Yes | MCP support inside OpenCode |
| Local voice | Yes | No |
| Self-hosted daemon | Yes | OpenCode server / local runtime |
See also: [Paseo vs Codex App](/alternatives/codex-app), [Paseo vs Claude Desktop](/alternatives/claude-desktop), [Paseo vs OpenChamber](/alternatives/openchamber).
@@ -0,0 +1,116 @@
---
title: Superset Alternative With Linux, Windows, and Mobile
description: Paseo is open source under an OSI license, has no login wall, ships native mobile, and runs on macOS, Linux, and Windows. Superset is source-available, macOS only, and gates the desktop app on a Superset login.
nav: Superset
order: 51
---
# Paseo vs Superset
Superset is a macOS desktop app for running CLI coding agents in parallel git worktrees. Source-available under the Elastic License 2.0.
Paseo is an app for orchestrating coding agents, with native clients on desktop, mobile, web, and the CLI. Open source (AGPL-3.0).
![Paseo desktop and mobile app](/hero-mockup.png)
## When to pick what
Pick Superset if you prefer a terminal-first interface where agents live inside terminal panes.
Pick Paseo if you want:
- An OSI-approved open source license (AGPL-3.0)
- Linux or Windows
- A native mobile app
- No login wall
- A per-agent UI with modes, slash commands, and file pickers
- Free without seat limits
## License
Paseo is open source under AGPL-3.0. Audit it, fork it, redistribute it.
Superset is source-available under the Elastic License 2.0. The source is on GitHub, but the license restricts hosting it as a service and limits redistribution.
## Login
Superset's desktop app shows a Superset login wall on first launch. A Superset account is required to use it.
Paseo does not require any login.
## Architecture
The Paseo daemon runs as its own process. Desktop, web, mobile, and CLI clients connect to it. Run the daemon on your laptop, on a server, or in Docker, and connect from anywhere.
Superset's desktop is the host. Agents run inside it.
## Providers
Both tools support many agents. Superset is a terminal multiplexer where each agent runs inside a terminal pane. Paseo runs Claude Code, Codex, OpenCode, and Pi natively with a per-agent UI (modes, slash commands, file picker, diff viewer), plus 30+ more agents through the in-app catalog via ACP, plus any custom CLI agent. See [Supported providers](/docs/supported-providers).
## Panes
Paseo's app has split panes and tabs. Panes include a diff viewer and a browser for testing running services. Agents render as native UI with modes, slash commands, and file pickers.
In Superset, each agent runs inside a terminal pane.
## GitHub
Paseo's app handles commit, push, opening PRs, watching checks and reviews, and merging.
## CLI
Paseo has a CLI that mirrors the app:
```bash
paseo run --provider codex "implement OAuth"
paseo run --host devbox:6767 "run the test suite"
paseo ls
paseo send <agent-id> "add tests"
paseo schedule create --cron "0 9 * * 1" "audit the codebase"
```
`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. `paseo loop` retries an agent until a verification command passes.
Superset is a desktop app and does not have a CLI.
## Worktrees and services
Both tools isolate parallel agents in git worktrees.
Paseo also gives each worktree its own dev server URL like `web.fix-auth.my-app.localhost`, so parallel agents don't fight for ports.
## Mobile
Paseo ships native iOS and Android apps with the same feature set as the desktop. Superset does not have a mobile app.
## Voice
Paseo's speech-to-text and text-to-speech run locally on your device. Superset does not have voice.
## Pricing
Paseo is free with no seat limits.
Superset is free for one seat with local workspaces only. Team features and sync start at $20 per seat per month.
## Comparison
| | Paseo | Superset |
| ---------------------------- | ----------------------------------------------------- | -------------------------------------- |
| License | Open source (AGPL-3.0) | Source-available (Elastic License 2.0) |
| Platforms | macOS, Linux, Windows | macOS only |
| Native mobile | iOS, Android | — |
| Login required | No | Yes (Superset account) |
| Pricing | Free | Free 1 seat, $20/seat/mo Pro |
| Per-agent native UI | Yes (modes, slash commands, file picker, diff viewer) | Terminal output |
| Split panes and tabs | Yes | Yes (terminals) |
| In-app browser | Yes | — |
| GitHub workflow in app | Commit, push, PR, checks, reviews, merge | Yes |
| Git worktrees | Yes | Yes |
| Per-worktree dev server URLs | Yes | — |
| CLI | Run, `--host`, ls, send, schedule, loop | — |
| Local voice (on-device) | Yes | — |
| Self-hosted daemon | Yes | — |
See also: [Paseo vs Conductor](/alternatives/conductor), [Paseo vs OpenChamber](/alternatives/openchamber), [Paseo vs Happy Coder](/alternatives/happy-coder).
@@ -0,0 +1,71 @@
export interface AlternativePage {
slug: string;
name: string;
href: string;
title: string;
description: string;
content: string;
order: number;
}
function parseFrontmatter(raw: string): { data: Record<string, string>; content: string } {
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
if (!match) return { data: {}, content: raw };
const data: Record<string, string> = {};
for (const line of match[1].split("\n")) {
const colonIdx = line.indexOf(":");
if (colonIdx === -1) continue;
const key = line.slice(0, colonIdx).trim();
const value = line
.slice(colonIdx + 1)
.trim()
.replace(/^["']|["']$/g, "");
data[key] = value;
}
return { data, content: match[2] };
}
const alternativeModules = import.meta.glob("../content/alternatives/*.md", {
eager: true,
query: "?raw",
import: "default",
}) as Record<string, string>;
function loadAlternativePages(): AlternativePage[] {
const pages: AlternativePage[] = [];
for (const [path, raw] of Object.entries(alternativeModules)) {
const fileName = path.split("/").pop() ?? "";
const slug = fileName.replace(/\.md$/, "");
const { data, content } = parseFrontmatter(raw);
const order = Number.parseInt(data.order ?? "999", 10);
pages.push({
slug,
name: data.nav ?? data.title ?? slug,
href: `/alternatives/${slug}`,
title: data.title ?? slug,
description: data.description ?? "",
content,
order: Number.isFinite(order) ? order : 999,
});
}
pages.sort((a, b) => a.order - b.order);
return pages;
}
let cached: AlternativePage[] | undefined;
export function getAlternativePages(): AlternativePage[] {
if (!cached) cached = loadAlternativePages();
return cached;
}
export function getAlternativePage(slug: string): AlternativePage | undefined {
return getAlternativePages().find((p) => p.slug === slug);
}
export const ALTERNATIVE_PAGE_SLUGS: readonly string[] = getAlternativePages().map((p) => p.slug);
+196 -1
View File
@@ -1,8 +1,17 @@
import Slugger from "github-slugger";
interface DocFrontmatter {
title: string;
description: string;
nav: string;
order: number;
category?: string;
}
export interface DocHeading {
depth: number;
text: string;
id: string;
}
export interface Doc {
@@ -11,8 +20,31 @@ export interface Doc {
sourcePath: string;
frontmatter: DocFrontmatter;
content: string;
headings: DocHeading[];
}
export type DocsNavNode =
| {
type: "category";
label: string;
children: DocsNavNode[];
order: number;
}
| {
type: "group";
segment: string;
label: string;
children: DocsNavNode[];
order: number;
}
| {
type: "page";
segment: string;
label: string;
href: string;
order: number;
};
function parseFrontmatter(raw: string): { data: Record<string, string>; content: string } {
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
if (!match) return { data: {}, content: raw };
@@ -32,6 +64,40 @@ function parseFrontmatter(raw: string): { data: Record<string, string>; content:
return { data, content: match[2] };
}
function stripInlineMarkdown(text: string): string {
return text
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
.replace(/!?\[([^\]]*)\]\[[^\]]*\]/g, "$1")
.replace(/`([^`]+)`/g, "$1")
.replace(/(\*\*|__)(.*?)\1/g, "$2")
.replace(/(\*|_)(.*?)\1/g, "$2")
.replace(/<[^>]+>/g, "")
.trim();
}
function parseHeadings(content: string): DocHeading[] {
const slugger = new Slugger();
const headings: DocHeading[] = [];
const regex = /^(#{1,6})\s+(.+)$/gm;
let match: RegExpExecArray | null;
while ((match = regex.exec(content)) !== null) {
const depth = match[1].length;
if (depth < 2 || depth > 4) continue;
const text = stripInlineMarkdown(match[2].trim());
if (!text) continue;
headings.push({
depth,
text,
id: slugger.slug(text),
});
}
return headings;
}
const docModules = import.meta.glob("../../../public-docs/**/*.md", {
eager: true,
query: "?raw",
@@ -41,7 +107,8 @@ const docModules = import.meta.glob("../../../public-docs/**/*.md", {
function pathToSlug(path: string): string {
const after = path.split("/public-docs/")[1] ?? path;
const noExt = after.replace(/\.md$/, "");
return noExt === "index" ? "" : noExt;
if (noExt === "index") return "";
return noExt.replace(/\/index$/, "");
}
function pathToSourcePath(path: string): string {
@@ -66,8 +133,10 @@ function loadDocs(): Doc[] {
description: data.description ?? "",
nav: data.nav ?? data.title ?? slug,
order: Number.isFinite(order) ? order : 999,
category: data.category,
},
content,
headings: parseHeadings(content),
});
}
@@ -85,3 +154,129 @@ export function getDocs(): Doc[] {
export function getDoc(slug: string): Doc | undefined {
return getDocs().find((d) => d.slug === slug);
}
function formatLabel(segment: string): string {
return segment.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
}
function insertDocByPath(nodes: DocsNavNode[], doc: Doc): void {
const relative = doc.sourcePath.replace(/^public-docs\//, "");
const segments = relative.replace(/\.md$/, "").split("/");
const fileName = segments.pop() ?? "";
const directories = segments;
let current = nodes;
for (const segment of directories) {
let group = current.find(
(node): node is Extract<DocsNavNode, { type: "group" }> =>
node.type === "group" && node.segment === segment,
);
if (!group) {
group = {
type: "group",
segment,
label: formatLabel(segment),
children: [],
order: doc.frontmatter.order,
};
current.push(group);
}
current = group.children;
}
const pageSegment = fileName === "index" ? (directories.at(-1) ?? "") : fileName;
current.push({
type: "page",
segment: pageSegment,
label: doc.frontmatter.nav,
href: doc.href,
order: doc.frontmatter.order,
});
}
function nodeOrder(node: DocsNavNode): number {
if (node.type === "page") return node.order;
if (node.children.length === 0) return Infinity;
let min = Infinity;
for (const child of node.children) {
const childOrder = nodeOrder(child);
if (childOrder < min) min = childOrder;
}
return min;
}
function sortNodes(nodes: DocsNavNode[]): void {
nodes.sort((a, b) => nodeOrder(a) - nodeOrder(b));
for (const node of nodes) {
if (node.type !== "page") {
sortNodes(node.children);
}
}
}
export function buildDocsNavTree(docs: Doc[]): DocsNavNode[] {
const byCategory = new Map<string, Doc[]>();
const uncategorized: Doc[] = [];
for (const doc of docs) {
const category = doc.frontmatter.category?.trim();
if (category) {
const list = byCategory.get(category) ?? [];
list.push(doc);
byCategory.set(category, list);
} else {
uncategorized.push(doc);
}
}
const root: DocsNavNode[] = [];
for (const doc of uncategorized) {
insertDocByPath(root, doc);
}
for (const [label, docsInCategory] of byCategory) {
const children: DocsNavNode[] = [];
for (const doc of docsInCategory) {
insertDocByPath(children, doc);
}
root.push({
type: "category",
label,
children,
order: nodeOrder({ type: "category", label, children, order: Infinity }),
});
}
sortNodes(root);
return root;
}
function findNodePath(
nodes: DocsNavNode[],
href: string,
path: DocsNavNode[] = [],
): DocsNavNode[] | null {
for (const node of nodes) {
if (node.type === "page" && node.href === href) {
return [...path, node];
}
if (node.type !== "page") {
const found = findNodePath(node.children, href, [...path, node]);
if (found) return found;
}
}
return null;
}
export function getDocBreadcrumbGroups(doc: Doc, tree: DocsNavNode[]): { label: string }[] {
const path = findNodePath(tree, doc.href);
if (!path) return [];
return path
.slice(0, -1)
.filter((node): node is Extract<DocsNavNode, { type: "group" }> => node.type === "group")
.map((node) => ({ label: node.label }));
}
+8 -5
View File
@@ -1,3 +1,4 @@
import { getAlternativePages } from "~/data/alternative-pages";
import { AGENT_PAGES } from "~/data/agent-pages";
import { type Doc, getDocs } from "~/docs";
@@ -27,17 +28,19 @@ function agentLine(agent: (typeof AGENT_PAGES)[number]): string {
return `- [${agent.name}](${SITE_URL}/${agent.slug}): ${agent.subtitle}`;
}
function alternativeLine(page: ReturnType<typeof getAlternativePages>[number]): string {
const description = page.description.trim();
const suffix = description ? `: ${description}` : "";
return `- [${page.title}](${SITE_URL}${page.href})${suffix}`;
}
function topLevelDocs(): Doc[] {
return getDocs().filter((d) => !d.slug.includes("/"));
}
function alternativeDocs(): Doc[] {
return getDocs().filter((d) => d.slug.startsWith("alternatives/"));
}
export function buildLlmsTxt(): string {
const docs = topLevelDocs().map(docLine).join("\n");
const alternatives = alternativeDocs().map(docLine).join("\n");
const alternatives = getAlternativePages().map(alternativeLine).join("\n");
const agents = AGENT_PAGES.map(agentLine).join("\n");
return `${PRODUCT_PREAMBLE}
+149
View File
@@ -61,6 +61,13 @@ import { Route as DocsIndexRouteImport } from "./routes/docs/index";
import { Route as BlogIndexRouteImport } from "./routes/blog/index";
import { Route as DocsSplatRouteImport } from "./routes/docs/$";
import { Route as BlogSplatRouteImport } from "./routes/blog/$";
import { Route as AlternativesSupersetRouteImport } from "./routes/alternatives/superset";
import { Route as AlternativesOpencodeDesktopRouteImport } from "./routes/alternatives/opencode-desktop";
import { Route as AlternativesOpenchamberRouteImport } from "./routes/alternatives/openchamber";
import { Route as AlternativesHappyCoderRouteImport } from "./routes/alternatives/happy-coder";
import { Route as AlternativesConductorRouteImport } from "./routes/alternatives/conductor";
import { Route as AlternativesCodexAppRouteImport } from "./routes/alternatives/codex-app";
import { Route as AlternativesClaudeDesktopRouteImport } from "./routes/alternatives/claude-desktop";
const VtcodeRoute = VtcodeRouteImport.update({
id: "/vtcode",
@@ -322,6 +329,43 @@ const BlogSplatRoute = BlogSplatRouteImport.update({
path: "/$",
getParentRoute: () => BlogRoute,
} as any);
const AlternativesSupersetRoute = AlternativesSupersetRouteImport.update({
id: "/alternatives/superset",
path: "/alternatives/superset",
getParentRoute: () => rootRouteImport,
} as any);
const AlternativesOpencodeDesktopRoute =
AlternativesOpencodeDesktopRouteImport.update({
id: "/alternatives/opencode-desktop",
path: "/alternatives/opencode-desktop",
getParentRoute: () => rootRouteImport,
} as any);
const AlternativesOpenchamberRoute = AlternativesOpenchamberRouteImport.update({
id: "/alternatives/openchamber",
path: "/alternatives/openchamber",
getParentRoute: () => rootRouteImport,
} as any);
const AlternativesHappyCoderRoute = AlternativesHappyCoderRouteImport.update({
id: "/alternatives/happy-coder",
path: "/alternatives/happy-coder",
getParentRoute: () => rootRouteImport,
} as any);
const AlternativesConductorRoute = AlternativesConductorRouteImport.update({
id: "/alternatives/conductor",
path: "/alternatives/conductor",
getParentRoute: () => rootRouteImport,
} as any);
const AlternativesCodexAppRoute = AlternativesCodexAppRouteImport.update({
id: "/alternatives/codex-app",
path: "/alternatives/codex-app",
getParentRoute: () => rootRouteImport,
} as any);
const AlternativesClaudeDesktopRoute =
AlternativesClaudeDesktopRouteImport.update({
id: "/alternatives/claude-desktop",
path: "/alternatives/claude-desktop",
getParentRoute: () => rootRouteImport,
} as any);
export interface FileRoutesByFullPath {
"/": typeof IndexRoute;
@@ -372,6 +416,13 @@ export interface FileRoutesByFullPath {
"/sponsor": typeof SponsorRoute;
"/stakpak": typeof StakpakRoute;
"/vtcode": typeof VtcodeRoute;
"/alternatives/claude-desktop": typeof AlternativesClaudeDesktopRoute;
"/alternatives/codex-app": typeof AlternativesCodexAppRoute;
"/alternatives/conductor": typeof AlternativesConductorRoute;
"/alternatives/happy-coder": typeof AlternativesHappyCoderRoute;
"/alternatives/openchamber": typeof AlternativesOpenchamberRoute;
"/alternatives/opencode-desktop": typeof AlternativesOpencodeDesktopRoute;
"/alternatives/superset": typeof AlternativesSupersetRoute;
"/blog/$": typeof BlogSplatRoute;
"/docs/$": typeof DocsSplatRoute;
"/blog/": typeof BlogIndexRoute;
@@ -424,6 +475,13 @@ export interface FileRoutesByTo {
"/sponsor": typeof SponsorRoute;
"/stakpak": typeof StakpakRoute;
"/vtcode": typeof VtcodeRoute;
"/alternatives/claude-desktop": typeof AlternativesClaudeDesktopRoute;
"/alternatives/codex-app": typeof AlternativesCodexAppRoute;
"/alternatives/conductor": typeof AlternativesConductorRoute;
"/alternatives/happy-coder": typeof AlternativesHappyCoderRoute;
"/alternatives/openchamber": typeof AlternativesOpenchamberRoute;
"/alternatives/opencode-desktop": typeof AlternativesOpencodeDesktopRoute;
"/alternatives/superset": typeof AlternativesSupersetRoute;
"/blog/$": typeof BlogSplatRoute;
"/docs/$": typeof DocsSplatRoute;
"/blog": typeof BlogIndexRoute;
@@ -479,6 +537,13 @@ export interface FileRoutesById {
"/sponsor": typeof SponsorRoute;
"/stakpak": typeof StakpakRoute;
"/vtcode": typeof VtcodeRoute;
"/alternatives/claude-desktop": typeof AlternativesClaudeDesktopRoute;
"/alternatives/codex-app": typeof AlternativesCodexAppRoute;
"/alternatives/conductor": typeof AlternativesConductorRoute;
"/alternatives/happy-coder": typeof AlternativesHappyCoderRoute;
"/alternatives/openchamber": typeof AlternativesOpenchamberRoute;
"/alternatives/opencode-desktop": typeof AlternativesOpencodeDesktopRoute;
"/alternatives/superset": typeof AlternativesSupersetRoute;
"/blog/$": typeof BlogSplatRoute;
"/docs/$": typeof DocsSplatRoute;
"/blog/": typeof BlogIndexRoute;
@@ -535,6 +600,13 @@ export interface FileRouteTypes {
| "/sponsor"
| "/stakpak"
| "/vtcode"
| "/alternatives/claude-desktop"
| "/alternatives/codex-app"
| "/alternatives/conductor"
| "/alternatives/happy-coder"
| "/alternatives/openchamber"
| "/alternatives/opencode-desktop"
| "/alternatives/superset"
| "/blog/$"
| "/docs/$"
| "/blog/"
@@ -587,6 +659,13 @@ export interface FileRouteTypes {
| "/sponsor"
| "/stakpak"
| "/vtcode"
| "/alternatives/claude-desktop"
| "/alternatives/codex-app"
| "/alternatives/conductor"
| "/alternatives/happy-coder"
| "/alternatives/openchamber"
| "/alternatives/opencode-desktop"
| "/alternatives/superset"
| "/blog/$"
| "/docs/$"
| "/blog"
@@ -641,6 +720,13 @@ export interface FileRouteTypes {
| "/sponsor"
| "/stakpak"
| "/vtcode"
| "/alternatives/claude-desktop"
| "/alternatives/codex-app"
| "/alternatives/conductor"
| "/alternatives/happy-coder"
| "/alternatives/openchamber"
| "/alternatives/opencode-desktop"
| "/alternatives/superset"
| "/blog/$"
| "/docs/$"
| "/blog/"
@@ -696,6 +782,13 @@ export interface RootRouteChildren {
SponsorRoute: typeof SponsorRoute;
StakpakRoute: typeof StakpakRoute;
VtcodeRoute: typeof VtcodeRoute;
AlternativesClaudeDesktopRoute: typeof AlternativesClaudeDesktopRoute;
AlternativesCodexAppRoute: typeof AlternativesCodexAppRoute;
AlternativesConductorRoute: typeof AlternativesConductorRoute;
AlternativesHappyCoderRoute: typeof AlternativesHappyCoderRoute;
AlternativesOpenchamberRoute: typeof AlternativesOpenchamberRoute;
AlternativesOpencodeDesktopRoute: typeof AlternativesOpencodeDesktopRoute;
AlternativesSupersetRoute: typeof AlternativesSupersetRoute;
}
declare module "@tanstack/react-router" {
@@ -1064,6 +1157,55 @@ declare module "@tanstack/react-router" {
preLoaderRoute: typeof BlogSplatRouteImport;
parentRoute: typeof BlogRoute;
};
"/alternatives/superset": {
id: "/alternatives/superset";
path: "/alternatives/superset";
fullPath: "/alternatives/superset";
preLoaderRoute: typeof AlternativesSupersetRouteImport;
parentRoute: typeof rootRouteImport;
};
"/alternatives/opencode-desktop": {
id: "/alternatives/opencode-desktop";
path: "/alternatives/opencode-desktop";
fullPath: "/alternatives/opencode-desktop";
preLoaderRoute: typeof AlternativesOpencodeDesktopRouteImport;
parentRoute: typeof rootRouteImport;
};
"/alternatives/openchamber": {
id: "/alternatives/openchamber";
path: "/alternatives/openchamber";
fullPath: "/alternatives/openchamber";
preLoaderRoute: typeof AlternativesOpenchamberRouteImport;
parentRoute: typeof rootRouteImport;
};
"/alternatives/happy-coder": {
id: "/alternatives/happy-coder";
path: "/alternatives/happy-coder";
fullPath: "/alternatives/happy-coder";
preLoaderRoute: typeof AlternativesHappyCoderRouteImport;
parentRoute: typeof rootRouteImport;
};
"/alternatives/conductor": {
id: "/alternatives/conductor";
path: "/alternatives/conductor";
fullPath: "/alternatives/conductor";
preLoaderRoute: typeof AlternativesConductorRouteImport;
parentRoute: typeof rootRouteImport;
};
"/alternatives/codex-app": {
id: "/alternatives/codex-app";
path: "/alternatives/codex-app";
fullPath: "/alternatives/codex-app";
preLoaderRoute: typeof AlternativesCodexAppRouteImport;
parentRoute: typeof rootRouteImport;
};
"/alternatives/claude-desktop": {
id: "/alternatives/claude-desktop";
path: "/alternatives/claude-desktop";
fullPath: "/alternatives/claude-desktop";
preLoaderRoute: typeof AlternativesClaudeDesktopRouteImport;
parentRoute: typeof rootRouteImport;
};
}
}
@@ -1140,6 +1282,13 @@ const rootRouteChildren: RootRouteChildren = {
SponsorRoute: SponsorRoute,
StakpakRoute: StakpakRoute,
VtcodeRoute: VtcodeRoute,
AlternativesClaudeDesktopRoute: AlternativesClaudeDesktopRoute,
AlternativesCodexAppRoute: AlternativesCodexAppRoute,
AlternativesConductorRoute: AlternativesConductorRoute,
AlternativesHappyCoderRoute: AlternativesHappyCoderRoute,
AlternativesOpenchamberRoute: AlternativesOpenchamberRoute,
AlternativesOpencodeDesktopRoute: AlternativesOpencodeDesktopRoute,
AlternativesSupersetRoute: AlternativesSupersetRoute,
};
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
@@ -0,0 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import { alternativeRouteOptions } from "~/components/alternative-page";
export const Route = createFileRoute("/alternatives/claude-desktop")(
alternativeRouteOptions("claude-desktop"),
);
@@ -0,0 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import { alternativeRouteOptions } from "~/components/alternative-page";
export const Route = createFileRoute("/alternatives/codex-app")(
alternativeRouteOptions("codex-app"),
);
@@ -0,0 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import { alternativeRouteOptions } from "~/components/alternative-page";
export const Route = createFileRoute("/alternatives/conductor")(
alternativeRouteOptions("conductor"),
);
@@ -0,0 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import { alternativeRouteOptions } from "~/components/alternative-page";
export const Route = createFileRoute("/alternatives/happy-coder")(
alternativeRouteOptions("happy-coder"),
);
@@ -0,0 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import { alternativeRouteOptions } from "~/components/alternative-page";
export const Route = createFileRoute("/alternatives/openchamber")(
alternativeRouteOptions("openchamber"),
);
@@ -0,0 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import { alternativeRouteOptions } from "~/components/alternative-page";
export const Route = createFileRoute("/alternatives/opencode-desktop")(
alternativeRouteOptions("opencode-desktop"),
);
@@ -0,0 +1,4 @@
import { createFileRoute } from "@tanstack/react-router";
import { alternativeRouteOptions } from "~/components/alternative-page";
export const Route = createFileRoute("/alternatives/superset")(alternativeRouteOptions("superset"));
+1 -1
View File
@@ -7,7 +7,7 @@ export const Route = createFileRoute("/blog")({
function BlogLayout() {
return (
<SiteShell>
<SiteShell width="default">
<Outlet />
</SiteShell>
);
+1 -1
View File
@@ -16,7 +16,7 @@ export const Route = createFileRoute("/changelog")({
function Changelog() {
return (
<SiteShell>
<SiteShell width="default">
<article className="changelog-markdown rounded-xl border border-border bg-card/40 p-6 md:p-8">
<ReactMarkdown>{changelogMarkdown}</ReactMarkdown>
</article>
+1 -1
View File
@@ -22,7 +22,7 @@ type Status = "idle" | "submitting" | "success" | "error";
function Cloud() {
return (
<SiteShell>
<SiteShell width="default">
<h1 className="text-3xl font-medium mb-3">Paseo Cloud</h1>
<p className="text-white/70 leading-relaxed mb-10">
For using Paseo across machines, with a team, or inside a company. Looking for design
+31 -90
View File
@@ -1,55 +1,23 @@
import { createFileRoute, Link, Outlet } from "@tanstack/react-router";
import { createFileRoute, Link, Outlet, useLocation } from "@tanstack/react-router";
import { Menu, X } from "lucide-react";
import { useCallback, useState } from "react";
import { type Doc, getDocs } from "~/docs";
import { useCallback, useMemo, useState } from "react";
import { DocsBreadcrumbs } from "~/components/docs-breadcrumbs";
import { DocsNav } from "~/components/docs-nav";
import { DocsOutline } from "~/components/docs-outline";
import { buildDocsNavTree, getDoc, getDocs } from "~/docs";
import "~/styles.css";
export const Route = createFileRoute("/docs")({
component: DocsLayout,
});
const ACTIVE_OPTIONS_EXACT = { exact: true };
const MOBILE_ACTIVE_PROPS = { className: "text-foreground" };
const DESKTOP_ACTIVE_PROPS = { className: "bg-muted text-foreground" };
interface NavItem {
name: string;
href: string;
}
interface NavGroup {
name: string | null;
items: NavItem[];
}
const GROUP_LABELS: Record<string, string> = {
alternatives: "Alternatives",
};
function groupKey(doc: Doc): string | null {
const idx = doc.slug.indexOf("/");
return idx === -1 ? null : doc.slug.slice(0, idx);
}
function buildNavigation(): NavGroup[] {
const groups = new Map<string | null, NavItem[]>();
for (const doc of getDocs()) {
const key = groupKey(doc);
const items = groups.get(key) ?? [];
items.push({ name: doc.frontmatter.nav, href: doc.href });
groups.set(key, items);
}
const ordered: NavGroup[] = [];
if (groups.has(null)) ordered.push({ name: null, items: groups.get(null)! });
for (const [key, items] of groups) {
if (key === null) continue;
ordered.push({ name: GROUP_LABELS[key] ?? key, items });
}
return ordered;
}
function DocsLayout() {
const groups = buildNavigation();
const location = useLocation();
const tree = useMemo(() => buildDocsNavTree(getDocs()), []);
const slug = location.pathname === "/docs" ? "" : location.pathname.slice("/docs/".length);
const doc = getDoc(slug);
const [mobileNavOpen, setMobileNavOpen] = useState(false);
const toggleMobileNav = useCallback(() => setMobileNavOpen((v) => !v), []);
const closeMobileNav = useCallback(() => setMobileNavOpen(false), []);
@@ -57,7 +25,7 @@ function DocsLayout() {
return (
<div className="min-h-screen bg-background">
{/* Mobile header */}
<header className="md:hidden sticky top-0 z-50 bg-background border-b border-border">
<header className="lg:hidden sticky top-0 z-50 bg-background border-b border-border">
<div className="flex items-center justify-between p-4">
<Link to="/" className="flex items-center gap-3">
<img src="/logo.svg" alt="Paseo" className="w-6 h-6" />
@@ -74,61 +42,34 @@ function DocsLayout() {
</button>
</div>
{mobileNavOpen && (
<nav className="border-t border-border px-4 py-4 space-y-4 max-h-[calc(100dvh-4rem)] overflow-y-auto">
{groups.map((group) => (
<div key={group.name ?? "root"} className="space-y-1">
{group.name && (
<div className="text-sm font-medium text-foreground mb-2">{group.name}</div>
)}
{group.items.map((item) => (
<Link
key={item.href}
to={item.href}
activeOptions={ACTIVE_OPTIONS_EXACT}
onClick={closeMobileNav}
className="block py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
activeProps={MOBILE_ACTIVE_PROPS}
>
{item.name}
</Link>
))}
</div>
))}
<nav className="border-t border-border px-4 py-4 max-h-[calc(100dvh-4rem)] overflow-y-auto">
<DocsNav nodes={tree} mobile onNavigate={closeMobileNav} />
</nav>
)}
</header>
<div className="flex">
<div className="max-w-[90rem] mx-auto flex items-start">
{/* Desktop sidebar */}
<aside className="hidden md:flex md:flex-col w-56 shrink-0 border-r border-border p-6 sticky top-0 h-screen">
<Link to="/" className="flex items-center gap-3 mb-8 shrink-0">
<aside className="hidden lg:block sticky top-0 h-screen w-60 shrink-0 border-r border-border p-6 overflow-y-auto">
<Link to="/" className="flex items-center gap-3 mb-8">
<img src="/logo.svg" alt="Paseo" className="w-6 h-6" />
<span className="text-lg font-medium">Paseo</span>
</Link>
<nav className="flex-1 min-h-0 overflow-y-auto -ml-3 -mr-3 pr-3 space-y-4">
{groups.map((group) => (
<div key={group.name ?? "root"} className="space-y-1">
{group.name && (
<div className="px-3 py-2 text-sm font-medium text-foreground">{group.name}</div>
)}
{group.items.map((item) => (
<Link
key={item.href}
to={item.href}
activeOptions={ACTIVE_OPTIONS_EXACT}
className="block px-3 py-2 text-sm rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
activeProps={DESKTOP_ACTIVE_PROPS}
>
{item.name}
</Link>
))}
</div>
))}
</nav>
<DocsNav nodes={tree} />
</aside>
<main className="flex-1 min-w-0 p-6 md:p-12 max-w-3xl docs-prose">
<Outlet />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 md:px-12 py-8 md:py-12">
<div className="max-w-prose mx-auto">
{doc && <DocsBreadcrumbs doc={doc} tree={tree} />}
<Outlet />
</div>
</main>
{/* Right outline */}
<aside className="hidden xl:block sticky top-0 h-screen w-60 shrink-0 px-2 overflow-y-auto">
{doc && <DocsOutline headings={doc.headings} />}
</aside>
</div>
</div>
);
+1 -1
View File
@@ -33,7 +33,7 @@ function Download() {
const urls = downloadUrls(release);
return (
<SiteShell>
<SiteShell width="default">
<h1 className="text-3xl md:text-4xl font-semibold tracking-tight mb-2">Download</h1>
<p className="text-muted-foreground mb-10">v{version}</p>
+1 -1
View File
@@ -14,7 +14,7 @@ export const Route = createFileRoute("/privacy")({
function Privacy() {
return (
<SiteShell>
<SiteShell width="default">
<h1 className="text-3xl font-medium mb-8">Privacy Policy</h1>
<div className="space-y-6 text-white/70 leading-relaxed">
+1 -1
View File
@@ -14,7 +14,7 @@ export const Route = createFileRoute("/sponsor")({
function Sponsor() {
return (
<SiteShell>
<SiteShell width="default">
<h1 className="text-3xl font-medium tracking-tight mb-8">Sponsor</h1>
{/* Founder note */}
+6
View File
@@ -33,6 +33,12 @@ export default {
return Response.redirect(url.toString(), 301);
}
const altRedirectMatch = url.pathname.match(/^\/docs\/alternatives\/(.+?)\/?$/);
if (altRedirectMatch) {
url.pathname = `/alternatives/${altRedirectMatch[1]}`;
return Response.redirect(url.toString(), 301);
}
if (url.pathname === "/llms.txt") {
return markdownResponse(buildLlmsTxt());
}
+17 -2
View File
@@ -22,8 +22,11 @@ function discoverDocsRoutes(): string[] {
continue;
}
if (!entry.name.endsWith(".md")) continue;
const rel = path.relative(docsDir, full).replace(/\.md$/, "");
if (rel === "index") continue;
const rel = path
.relative(docsDir, full)
.replace(/\.md$/, "")
.replace(/\/index$/, "");
if (rel === "index" || rel === "") continue;
routes.add(`/docs/${rel.split(path.sep).join("/")}`);
}
};
@@ -54,6 +57,17 @@ function discoverAgentRoutes(): string[] {
.map((slug) => `/${slug}`);
}
function discoverAlternativeRoutes(): string[] {
const alternativesDir = path.join(__dirname, "src/routes/alternatives");
if (!fs.existsSync(alternativesDir)) return [];
const slugs = fs
.readdirSync(alternativesDir, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith(".tsx"))
.map((entry) => entry.name.replace(/\.tsx$/, ""))
.sort();
return ["/alternatives", ...slugs.map((slug) => `/alternatives/${slug}`)];
}
function discoverBlogRoutes(): string[] {
const postsDir = path.join(__dirname, "posts");
if (!fs.existsSync(postsDir)) return ["/blog"];
@@ -73,6 +87,7 @@ const sitemapPages = [
"/download",
"/privacy",
...discoverAgentRoutes(),
...discoverAlternativeRoutes(),
...discoverDocsRoutes(),
...discoverBlogRoutes(),
].map((routePath) => ({