feat(pages): add per-chapter routes for docs (#294)

Each docs chapter previously lived at the single /docs route driven by
component state, so chapters had no shareable URL and browser back/forward
did not work. Add a /docs/:slug route and derive activeSlug from the URL
param (falling back to quickstart), navigating via the router on switch.

Also add a dev-time invariant that throws when two sidebar entries share a
slug, since each slug now maps to exactly one URL.
This commit is contained in:
kite 2026-07-03 20:29:29 +08:00 committed by GitHub
parent b82fcc31f2
commit 1587630604
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 21 additions and 5 deletions

View file

@ -23,6 +23,7 @@ const App: React.FC = () => {
<Route path="/benchmark" element={<LandingPage><BenchmarkPage /></LandingPage>} />
<Route path="/quickstart" element={<LandingPage><QuickStartPage /></LandingPage>} />
<Route path="/docs" element={<DocsPage />} />
<Route path="/docs/:slug" element={<DocsPage />} />
</Routes>
</>
);

View file

@ -1,4 +1,5 @@
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useTranslation } from '../i18n';
import Navbar from '../components/Navbar';
import Footer from '../components/Footer';
@ -109,9 +110,24 @@ function buildFlatDocList(): { slug: DocSlug; labelKey: string }[] {
}
const flatDocList = buildFlatDocList();
const validSlugs = new Set<DocSlug>(flatDocList.map(d => d.slug));
/* Dev-time invariant: every sidebar slug maps to exactly one URL, so duplicates
* (two menu entries sharing a slug) would silently collide. Fail loudly in dev. */
if (process.env.NODE_ENV !== 'production' && validSlugs.size !== flatDocList.length) {
const slugs = flatDocList.map(d => d.slug);
const dupes = [...new Set(slugs.filter((s, i) => slugs.indexOf(s) !== i))];
throw new Error(
`[docs] Duplicate sidebar slug(s) detected: ${dupes.join(', ')} — each doc must have a unique slug for routing.`
);
}
const DocsPage: React.FC = () => {
const [activeSlug, setActiveSlug] = useState<DocSlug>('quickstart');
const { slug: slugParam } = useParams<{ slug?: string }>();
const navigate = useNavigate();
/* Active doc slug is derived from the URL param, falling back to quickstart */
const activeSlug: DocSlug =
slugParam && validSlugs.has(slugParam as DocSlug) ? (slugParam as DocSlug) : 'quickstart';
const [expandedItems, setExpandedItems] = useState<Record<string, boolean>>({ 'sb-integrations': false });
const [activeHeadingId, setActiveHeadingId] = useState<string>('');
const [hoveredHeadingId, setHoveredHeadingId] = useState<string>('');
@ -162,10 +178,10 @@ const DocsPage: React.FC = () => {
}, []);
const navigateToDoc = useCallback((slug: DocSlug) => {
setActiveSlug(slug);
navigate(`/docs/${slug}`);
// Scroll page to top
window.scrollTo(0, 0);
}, []);
}, [navigate]);
/* Intercept clicks on internal doc links and convert to SPA navigation */
const handleContentClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
@ -194,8 +210,7 @@ const DocsPage: React.FC = () => {
const slugMap: Record<string, DocSlug> = { 'ci': 'cicd' };
const slug = (slugMap[lastSegment] || lastSegment) as DocSlug;
// Verify it's a valid doc slug
const validSlugs = flatDocList.map(d => d.slug);
if (validSlugs.includes(slug)) {
if (validSlugs.has(slug)) {
e.preventDefault();
navigateToDoc(slug);
// Handle anchor scroll after navigation with reliable retry