diff --git a/packages/docs/src/App.tsx b/packages/docs/src/App.tsx
index c47c3f0..40a0cae 100644
--- a/packages/docs/src/App.tsx
+++ b/packages/docs/src/App.tsx
@@ -3,6 +3,7 @@ import { PageMeta } from "./pageMeta.js";
import { route } from "@funstack/router/server";
import { defer } from "@funstack/static/server";
import { Layout } from "./components/Layout.js";
+import { DocsLayout } from "./components/DocsLayout.js";
import { ApiReferencePage } from "./pages/ApiReferencePage.js";
import { LearnPage } from "./pages/LearnPage.js";
import { ClientApp } from "./ClientApp.js";
@@ -142,160 +143,171 @@ export const routes = [
component: defer(withMeta("/", ), { name: "HomePage" }),
}),
route({
- path: "/getting-started",
- component: defer(withMeta("/getting-started", ), {
- name: "GettingStartedPage",
- }),
- }),
- route({
- path: "/learn",
- component: LearnPage,
+ component: ,
children: [
route({
- path: "/",
- component: defer(withMeta("/learn", ), {
- name: "LearnIndexPage",
- }),
- }),
- route({
- path: "/navigation-api",
- component: defer(withMeta("/learn/navigation-api", ), {
- name: "LearnNavigationApiPage",
+ path: "/getting-started",
+ component: defer(withMeta("/getting-started", ), {
+ name: "GettingStartedPage",
}),
}),
route({
- path: "/nested-routes",
- component: defer(withMeta("/learn/nested-routes", ), {
- name: "LearnNestedRoutesPage",
- }),
- }),
- route({
- path: "/type-safety",
- component: defer(withMeta("/learn/type-safety", ), {
- name: "LearnTypeSafetyPage",
- }),
- }),
- route({
- path: "/ssr",
- component: ,
+ path: "/learn",
+ component: LearnPage,
children: [
route({
path: "/",
- component: defer(withMeta("/learn/ssr", ), {
- name: "LearnSsrBasicPage",
+ component: defer(withMeta("/learn", ), {
+ name: "LearnIndexPage",
}),
}),
route({
- path: "/static-site-generation",
+ path: "/navigation-api",
component: defer(
- withMeta("/learn/ssr/static-site-generation", ),
+ withMeta("/learn/navigation-api", ),
{
- name: "LearnSsgPage",
+ name: "LearnNavigationApiPage",
},
),
}),
route({
- path: "/with-loaders",
+ path: "/nested-routes",
+ component: defer(withMeta("/learn/nested-routes", ), {
+ name: "LearnNestedRoutesPage",
+ }),
+ }),
+ route({
+ path: "/type-safety",
+ component: defer(withMeta("/learn/type-safety", ), {
+ name: "LearnTypeSafetyPage",
+ }),
+ }),
+ route({
+ path: "/ssr",
+ component: ,
+ children: [
+ route({
+ path: "/",
+ component: defer(withMeta("/learn/ssr", ), {
+ name: "LearnSsrBasicPage",
+ }),
+ }),
+ route({
+ path: "/static-site-generation",
+ component: defer(
+ withMeta("/learn/ssr/static-site-generation", ),
+ {
+ name: "LearnSsgPage",
+ },
+ ),
+ }),
+ route({
+ path: "/with-loaders",
+ component: defer(
+ withMeta("/learn/ssr/with-loaders", ),
+ {
+ name: "LearnSsrWithLoadersPage",
+ },
+ ),
+ }),
+ ],
+ }),
+ route({
+ path: "/rsc",
+ component: ,
+ children: [
+ route({
+ path: "/",
+ component: defer(withMeta("/learn/rsc", ), {
+ name: "LearnRscPage",
+ }),
+ }),
+ route({
+ path: "/route-features",
+ component: defer(
+ withMeta("/learn/rsc/route-features", ),
+ {
+ name: "LearnRouteDefinitionsPage",
+ },
+ ),
+ }),
+ ],
+ }),
+ route({
+ path: "/actions",
+ component: defer(withMeta("/learn/actions", ), {
+ name: "LearnActionsPage",
+ }),
+ }),
+ route({
+ path: "/error-handling",
component: defer(
- withMeta("/learn/ssr/with-loaders", ),
+ withMeta("/learn/error-handling", ),
{
- name: "LearnSsrWithLoadersPage",
+ name: "LearnErrorHandlingPage",
},
),
}),
+ route({
+ path: "/transitions",
+ component: defer(withMeta("/learn/transitions", ), {
+ name: "LearnTransitionsPage",
+ }),
+ }),
+ route({
+ path: "/loaders",
+ component: defer(withMeta("/learn/loaders", ), {
+ name: "LearnLoadersPage",
+ }),
+ }),
],
}),
route({
- path: "/rsc",
- component: ,
+ path: "/api",
+ component: ApiReferencePage,
children: [
route({
path: "/",
- component: defer(withMeta("/learn/rsc", ), {
- name: "LearnRscPage",
+ component: defer(withMeta("/api", ), {
+ name: "ApiReferenceIndexPage",
}),
}),
route({
- path: "/route-features",
- component: defer(
- withMeta("/learn/rsc/route-features", ),
- {
- name: "LearnRouteDefinitionsPage",
- },
- ),
+ path: "/components",
+ component: defer(withMeta("/api/components", ), {
+ name: "ApiComponentsPage",
+ }),
+ }),
+ route({
+ path: "/hooks",
+ component: defer(withMeta("/api/hooks", ), {
+ name: "ApiHooksPage",
+ }),
+ }),
+ route({
+ path: "/utilities",
+ component: defer(withMeta("/api/utilities", ), {
+ name: "ApiUtilitiesPage",
+ }),
+ }),
+ route({
+ path: "/types",
+ component: defer(withMeta("/api/types", ), {
+ name: "ApiTypesPage",
+ }),
}),
],
}),
route({
- path: "/actions",
- component: defer(withMeta("/learn/actions", ), {
- name: "LearnActionsPage",
- }),
+ path: "/examples",
+ component: defer(withMeta("/examples", ), { name: "ExamplesPage" }),
}),
route({
- path: "/error-handling",
- component: defer(withMeta("/learn/error-handling", ), {
- name: "LearnErrorHandlingPage",
- }),
- }),
- route({
- path: "/transitions",
- component: defer(withMeta("/learn/transitions", ), {
- name: "LearnTransitionsPage",
- }),
- }),
- route({
- path: "/loaders",
- component: defer(withMeta("/learn/loaders", ), {
- name: "LearnLoadersPage",
- }),
+ path: "/faq",
+ component: defer(withMeta("/faq", ), { name: "FaqPage" }),
}),
],
}),
- route({
- path: "/api",
- component: ApiReferencePage,
- children: [
- route({
- path: "/",
- component: defer(withMeta("/api", ), {
- name: "ApiReferenceIndexPage",
- }),
- }),
- route({
- path: "/components",
- component: defer(withMeta("/api/components", ), {
- name: "ApiComponentsPage",
- }),
- }),
- route({
- path: "/hooks",
- component: defer(withMeta("/api/hooks", ), {
- name: "ApiHooksPage",
- }),
- }),
- route({
- path: "/utilities",
- component: defer(withMeta("/api/utilities", ), {
- name: "ApiUtilitiesPage",
- }),
- }),
- route({
- path: "/types",
- component: defer(withMeta("/api/types", ), {
- name: "ApiTypesPage",
- }),
- }),
- ],
- }),
- route({
- path: "/examples",
- component: defer(withMeta("/examples", ), { name: "ExamplesPage" }),
- }),
- route({
- path: "/faq",
- component: defer(withMeta("/faq", ), { name: "FaqPage" }),
- }),
route({
path: "/*",
component: defer(withMeta("/*", ), { name: "NotFoundPage" }),
diff --git a/packages/docs/src/components/DocsLayout.tsx b/packages/docs/src/components/DocsLayout.tsx
new file mode 100644
index 0000000..28c0af6
--- /dev/null
+++ b/packages/docs/src/components/DocsLayout.tsx
@@ -0,0 +1,14 @@
+import { Outlet } from "@funstack/router";
+import { DocsSidebar } from "./DocsSidebar.js";
+
+// Layout for documentation pages: a site-wide sidebar next to the page content.
+export function DocsLayout() {
+ return (
+
+ );
+}
diff --git a/packages/docs/src/components/DocsNav.tsx b/packages/docs/src/components/DocsNav.tsx
new file mode 100644
index 0000000..f59e774
--- /dev/null
+++ b/packages/docs/src/components/DocsNav.tsx
@@ -0,0 +1,84 @@
+"use client";
+
+import { useLocation } from "@funstack/router";
+import { docsNavSections, isNavGroup, type NavItem } from "../navigation.js";
+
+function DocsNavLink({
+ item,
+ currentPath,
+ onNavigate,
+}: {
+ item: NavItem;
+ currentPath: string;
+ onNavigate?: () => void;
+}) {
+ if (item.external) {
+ return (
+
+
+ {item.label} ↗
+
+
+ );
+ }
+ const isActive = currentPath === item.path;
+ return (
+
+
+ {item.label}
+
+
+ );
+}
+
+// Lists all documentation pages grouped by section, highlighting the current page.
+export function DocsNav({ onNavigate }: { onNavigate?: () => void }) {
+ const { pathname: currentPath } = useLocation();
+
+ return (
+ <>
+ {docsNavSections.map((section) => (
+
+
{section.label}
+
+ {section.items.map((entry) =>
+ isNavGroup(entry) ? (
+ -
+ {entry.label}
+
+ {entry.items.map((item) => (
+
+ ))}
+
+
+ ) : (
+
+ ),
+ )}
+
+
+ ))}
+ >
+ );
+}
diff --git a/packages/docs/src/components/DocsSidebar.tsx b/packages/docs/src/components/DocsSidebar.tsx
new file mode 100644
index 0000000..a299cee
--- /dev/null
+++ b/packages/docs/src/components/DocsSidebar.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+import { DocsNav } from "./DocsNav.js";
+
+export function DocsSidebar() {
+ const sidebarRef = useRef(null);
+
+ // On initial load, scroll the sidebar so that the current page's link is visible.
+ // Later navigations keep the sidebar's scroll position since the user clicked a visible link.
+ useEffect(() => {
+ const sidebar = sidebarRef.current;
+ const activeLink = sidebar?.querySelector(".docs-nav-link.active");
+ if (!sidebar || !activeLink) {
+ return;
+ }
+ const sidebarRect = sidebar.getBoundingClientRect();
+ const linkRect = activeLink.getBoundingClientRect();
+ if (linkRect.top < sidebarRect.top || linkRect.bottom > sidebarRect.bottom) {
+ sidebar.scrollTop +=
+ linkRect.top - sidebarRect.top - (sidebarRect.height - linkRect.height) / 2;
+ }
+ }, []);
+
+ return (
+
+ );
+}
diff --git a/packages/docs/src/components/Header.tsx b/packages/docs/src/components/Header.tsx
index 3dad82b..3e3b5ea 100644
--- a/packages/docs/src/components/Header.tsx
+++ b/packages/docs/src/components/Header.tsx
@@ -2,6 +2,8 @@
import { useLocation } from "@funstack/router";
import { useState } from "react";
+import { GITHUB_URL } from "../navigation.js";
+import { DocsNav } from "./DocsNav.js";
const navItems = [
{ path: "/", label: "Home" },
@@ -51,34 +53,28 @@ export function Header() {
))}
-
+
GitHub
{isMenuOpen && (
-