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
@@ -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 */}