diff --git a/website/src/app/docs/[...slug]/page.tsx b/website/src/app/docs/[...slug]/page.tsx
index 55693042..9d1369a8 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 86f405a2..b0f287ef 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 ef2c5baa..94dc6609 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 */}
-