From 51cde804f9a15bf8b032bf517185402e58d7f502 Mon Sep 17 00:00:00 2001 From: Ajit Pratap Singh Date: Wed, 14 Oct 2026 23:47:31 +0530 Subject: [PATCH 1/3] feat(website/docs): docs navigation, edit link, last updated, heading search Single Sidebar mount with inline mobile menu bar (no FAB), truncated breadcrumb using the sidebar label, collapsible On this page below xl, Edit this page / Last updated / version badge, category-aware prev/next and a build-time search index with h2/h3 deep links. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RkzVu3JY9Y6BRWfjesWts9 --- website/src/app/docs/[...slug]/page.tsx | 56 ++++++++++---- .../src/app/docs/search-index.json/route.ts | 30 ++++++++ website/src/components/docs/DocNavigation.tsx | 3 + website/src/components/docs/Sidebar.tsx | 56 ++++++++++---- website/src/components/docs/Toc.tsx | 41 ++++++++-- website/src/components/ui/SearchModal.tsx | 50 +++++++++--- website/src/lib/docs.ts | 77 +++++++++++++++---- website/src/lib/search-index.ts | 36 +++------ 8 files changed, 259 insertions(+), 90 deletions(-) create mode 100644 website/src/app/docs/search-index.json/route.ts diff --git a/website/src/app/docs/[...slug]/page.tsx b/website/src/app/docs/[...slug]/page.tsx index 4383e024..0882d729 100644 --- a/website/src/app/docs/[...slug]/page.tsx +++ b/website/src/app/docs/[...slug]/page.tsx @@ -7,6 +7,7 @@ import remarkGfm from 'remark-gfm'; import rehypeSlug from 'rehype-slug'; import { getDocBySlug, getDocSlugs, getAdjacentDocs, extractHeadings, stripCodeFences } from '@/lib/docs'; import { DOCS_SIDEBAR } from '@/lib/constants'; +import { PROJECT } from '@/data/metrics'; import { Sidebar } from '@/components/docs/Sidebar'; import { Toc } from '@/components/docs/Toc'; import { DocNavigation } from '@/components/docs/DocNavigation'; @@ -18,6 +19,15 @@ function getReadingTime(text: string): number { return Math.max(1, Math.ceil(words / 200)); } +function formatDate(iso: string): string { + return new Date(iso).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }); +} + interface PageProps { params: Promise<{ slug: string[] }>; } @@ -104,44 +114,57 @@ export default async function DocPage({ params }: PageProps) { <>
- {/* Sidebar */} -
- -
- {/* Mobile sidebar */} -
- -
+
+ {/* Main content */}
{/* Breadcrumbs */} - {/* Reading time */} -
+ {/* Meta: reading time, last updated, version, edit link */} +
+ {readingTime} min read + + + Last updated{' '} + + + + {PROJECT.version} + + + Edit this page +
+ + {/* Content */}
{content} @@ -156,6 +179,7 @@ export default async function DocPage({ params }: PageProps) {
+
); } diff --git a/website/src/app/docs/search-index.json/route.ts b/website/src/app/docs/search-index.json/route.ts new file mode 100644 index 00000000..3ed4eb13 --- /dev/null +++ b/website/src/app/docs/search-index.json/route.ts @@ -0,0 +1,30 @@ +import { DOCS_SIDEBAR } from '@/lib/constants'; +import { extractHeadings, getDocBySlug } from '@/lib/docs'; +import type { SearchEntry } from '@/lib/search-index'; + +// Generated once at build time; fetched lazily by the search modal. +export const dynamic = 'force-static'; + +export function GET() { + const entries: SearchEntry[] = []; + + for (const group of DOCS_SIDEBAR) { + for (const item of group.items) { + const base = { + slug: item.slug, + title: item.label, + category: group.category, + description: item.description, + }; + entries.push(base); + + const doc = getDocBySlug(item.slug); + if (!doc) continue; + for (const h of extractHeadings(doc.content)) { + entries.push({ slug: base.slug, title: base.title, category: base.category, heading: h.text, anchor: h.id }); + } + } + } + + return Response.json(entries); +} diff --git a/website/src/components/docs/DocNavigation.tsx b/website/src/components/docs/DocNavigation.tsx index fead0959..0342df20 100644 --- a/website/src/components/docs/DocNavigation.tsx +++ b/website/src/components/docs/DocNavigation.tsx @@ -3,6 +3,7 @@ import Link from 'next/link'; interface NavItem { slug: string; title: string; + category?: string; } interface DocNavigationProps { @@ -33,6 +34,7 @@ export function DocNavigation({ prev, next }: DocNavigationProps) { {prev.title} + {prev.category && {prev.category}} ) : (
@@ -57,6 +59,7 @@ export function DocNavigation({ prev, next }: DocNavigationProps) { + {next.category && {next.category}} ) : (
diff --git a/website/src/components/docs/Sidebar.tsx b/website/src/components/docs/Sidebar.tsx index 920bbb62..c68853cf 100644 --- a/website/src/components/docs/Sidebar.tsx +++ b/website/src/components/docs/Sidebar.tsx @@ -76,36 +76,60 @@ export function Sidebar() { ); + useEffect(() => { + if (!mobileOpen) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') setMobileOpen(false); + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [mobileOpen]); + return ( - <> - {/* Mobile toggle */} - + + Docs menu + +
{/* Mobile overlay */} {mobileOpen && (
setMobileOpen(false)} /> )} - {/* Sidebar */} + {/* Sidebar (sticky column on lg, drawer below) */} - +
); } diff --git a/website/src/components/docs/Toc.tsx b/website/src/components/docs/Toc.tsx index dafd987a..9a588d8c 100644 --- a/website/src/components/docs/Toc.tsx +++ b/website/src/components/docs/Toc.tsx @@ -4,9 +4,11 @@ import { useEffect, useState } from 'react'; interface TocProps { headings: { id: string; text: string; level: number }[]; + /** `rail` is the sticky xl sidebar; `collapsible` is the inline disclosure shown below xl. */ + variant?: 'rail' | 'collapsible'; } -export function Toc({ headings }: TocProps) { +export function Toc({ headings, variant = 'rail' }: TocProps) { const [activeId, setActiveId] = useState(''); useEffect(() => { @@ -36,11 +38,7 @@ export function Toc({ headings }: TocProps) { if (headings.length === 0) return null; - return ( -