diff --git a/src/components/overrides/Sidebar.astro b/src/components/overrides/Sidebar.astro index 8ffb96337..1c3148072 100644 --- a/src/components/overrides/Sidebar.astro +++ b/src/components/overrides/Sidebar.astro @@ -1,18 +1,69 @@ --- /** * Custom Sidebar component that mimics MkDocs Material theme's navigation.sections behavior. + * Includes an API language switcher (Python/TypeScript) that only appears on API pages. */ import MobileMenuFooter from 'virtual:starlight/components/MobileMenuFooter'; import SidebarPersister from '@astrojs/starlight/components/SidebarPersister.astro'; import SidebarSublist from './SidebarSublist.astro'; +import { pathWithBase } from '../../util/links'; -const { sidebar } = Astro.locals.starlightRoute; +const { sidebar, id: currentSlug } = Astro.locals.starlightRoute; +const isApiPage = currentSlug.startsWith('docs/api/'); +const isPython = currentSlug.startsWith('docs/api/python'); --- + {isApiPage && ( +
+ Python + TypeScript +
+ )}
+ + diff --git a/src/config/navbar.ts b/src/config/navbar.ts index 808fc9b19..c0447d143 100644 --- a/src/config/navbar.ts +++ b/src/config/navbar.ts @@ -20,6 +20,12 @@ export interface NavLink { * Example: href="/user-guide/quickstart/" but basePath="/user-guide/" */ basePath?: string + /** + * Additional base paths that should also be considered part of this nav section. + * Useful when a nav item encompasses multiple top-level sidebar sections. + * Example: Community nav item also covers /docs/labs/ and /docs/contribute/ + */ + additionalBasePaths?: string[] /** Set to true for external links (opens in new tab) */ external?: boolean } @@ -54,6 +60,7 @@ function transformNavLinks(links: NavLink[]): NavLink[] { ...link, href: withBase(link.href), ...(link.basePath ? { basePath: withBase(link.basePath) } : {}), + ...(link.additionalBasePaths ? { additionalBasePaths: link.additionalBasePaths.map(withBase) } : {}), } }) } diff --git a/src/config/navigation.yml b/src/config/navigation.yml index 63a0ead45..278c22350 100644 --- a/src/config/navigation.yml +++ b/src/config/navigation.yml @@ -12,8 +12,8 @@ navbar: - label: Home href: / - - label: User Guide - href: /docs/user-guide/quickstart/overview/ + - label: Docs + href: /docs/user-guide/quickstart/python/ basePath: /docs/user-guide/ - label: Examples href: /docs/examples/ @@ -21,49 +21,53 @@ navbar: - label: Community href: /docs/community/community-packages/ basePath: /docs/community/ - - label: Labs - href: /docs/labs/ - basePath: /docs/labs/ - - label: Blog - href: /blog/ - basePath: /blog/ - - label: Contribute ❤️ - href: /docs/contribute/ - basePath: /docs/contribute/ - - label: Python API + additionalBasePaths: + - /docs/labs/ + - /docs/contribute/ + - label: API Reference href: /docs/api/python/ - basePath: /docs/api/python/ - - label: TypeScript API - href: /docs/api/typescript/ - basePath: /docs/api/typescript/ + basePath: /docs/api/ sidebar: - - label: User Guide + - label: Docs items: - docs/index - - label: Quickstart + - label: Get Started items: - docs/user-guide/quickstart/overview - - docs/user-guide/quickstart/python - - docs/user-guide/quickstart/typescript + - label: "Quickstart: Python" + slug: docs/user-guide/quickstart/python + - label: "Quickstart: TypeScript" + slug: docs/user-guide/quickstart/typescript - docs/user-guide/build-with-ai + - label: Build + items: + - label: "Adding Tools" + slug: docs/user-guide/concepts/tools + - label: "Creating Custom Tools" + slug: docs/user-guide/concepts/tools/custom-tools + - label: "Using MCP Tools" + slug: docs/user-guide/concepts/tools/mcp-tools + - label: "Multi-Agent Systems" + slug: docs/user-guide/concepts/multi-agent/multi-agent-patterns + - label: "Structured Output" + slug: docs/user-guide/concepts/agents/structured-output + - label: "Streaming Responses" + slug: docs/user-guide/concepts/streaming + - label: "Voice & Realtime" + slug: docs/user-guide/concepts/bidirectional-streaming/quickstart - label: Concepts items: - - label: Agents - items: - - docs/user-guide/concepts/agents/agent-loop - - docs/user-guide/concepts/agents/state - - docs/user-guide/concepts/agents/session-management - - docs/user-guide/concepts/agents/prompts - - docs/user-guide/concepts/agents/retry-strategies - - docs/user-guide/concepts/agents/hooks - - docs/user-guide/concepts/agents/structured-output - - docs/user-guide/concepts/agents/conversation-management + - docs/user-guide/concepts/agents/agent-loop + - docs/user-guide/concepts/agents/state + - docs/user-guide/concepts/agents/session-management + - docs/user-guide/concepts/agents/prompts + - docs/user-guide/concepts/agents/hooks + - docs/user-guide/concepts/agents/conversation-management + - docs/user-guide/concepts/agents/retry-strategies + - docs/user-guide/concepts/interrupts - label: Tools items: - - docs/user-guide/concepts/tools - - docs/user-guide/concepts/tools/custom-tools - - docs/user-guide/concepts/tools/mcp-tools - docs/user-guide/concepts/tools/executors - docs/user-guide/concepts/tools/community-tools-package - label: Plugins @@ -71,25 +75,8 @@ sidebar: - docs/user-guide/concepts/plugins - docs/user-guide/concepts/plugins/skills - docs/user-guide/concepts/plugins/steering - - label: Model Providers - items: - - docs/user-guide/concepts/model-providers - - docs/user-guide/concepts/model-providers/amazon-bedrock - - docs/user-guide/concepts/model-providers/amazon-nova - - docs/user-guide/concepts/model-providers/anthropic - - docs/user-guide/concepts/model-providers/gemini - - docs/user-guide/concepts/model-providers/litellm - - docs/user-guide/concepts/model-providers/llamacpp - - docs/user-guide/concepts/model-providers/llamaapi - - docs/user-guide/concepts/model-providers/mistral - - docs/user-guide/concepts/model-providers/ollama - - docs/user-guide/concepts/model-providers/openai - - docs/user-guide/concepts/model-providers/sagemaker - - docs/user-guide/concepts/model-providers/writer - - docs/user-guide/concepts/model-providers/custom_model_provider - label: Streaming items: - - docs/user-guide/concepts/streaming - docs/user-guide/concepts/streaming/async-iterators - docs/user-guide/concepts/streaming/callback-handlers - label: Multi-agent @@ -99,11 +86,8 @@ sidebar: - docs/user-guide/concepts/multi-agent/swarm - docs/user-guide/concepts/multi-agent/graph - docs/user-guide/concepts/multi-agent/workflow - - docs/user-guide/concepts/multi-agent/multi-agent-patterns - - docs/user-guide/concepts/interrupts - label: Bidirectional Streaming items: - - docs/user-guide/concepts/bidirectional-streaming/quickstart - docs/user-guide/concepts/bidirectional-streaming/agent - label: Models items: @@ -118,6 +102,47 @@ sidebar: - label: Experimental items: - docs/user-guide/concepts/experimental/agent-config + - label: Model Providers + items: + - docs/user-guide/concepts/model-providers + - docs/user-guide/concepts/model-providers/amazon-bedrock + - docs/user-guide/concepts/model-providers/amazon-nova + - docs/user-guide/concepts/model-providers/anthropic + - docs/user-guide/concepts/model-providers/gemini + - docs/user-guide/concepts/model-providers/litellm + - docs/user-guide/concepts/model-providers/llamacpp + - docs/user-guide/concepts/model-providers/llamaapi + - docs/user-guide/concepts/model-providers/mistral + - docs/user-guide/concepts/model-providers/ollama + - docs/user-guide/concepts/model-providers/openai + - docs/user-guide/concepts/model-providers/sagemaker + - docs/user-guide/concepts/model-providers/writer + - docs/user-guide/concepts/model-providers/custom_model_provider + - docs/user-guide/concepts/model-providers/cohere + - docs/user-guide/concepts/model-providers/clova-studio + - docs/user-guide/concepts/model-providers/fireworksai + - docs/user-guide/concepts/model-providers/nebius-token-factory + - docs/user-guide/concepts/model-providers/xai + - label: Deploy + items: + - docs/user-guide/deploy/operating-agents-in-production + - label: Amazon Bedrock AgentCore + items: + - docs/user-guide/deploy/deploy_to_bedrock_agentcore + - docs/user-guide/deploy/deploy_to_bedrock_agentcore/python + - docs/user-guide/deploy/deploy_to_bedrock_agentcore/typescript + - docs/user-guide/deploy/deploy_to_aws_lambda + - docs/user-guide/deploy/deploy_to_aws_fargate + - docs/user-guide/deploy/deploy_to_aws_apprunner + - docs/user-guide/deploy/deploy_to_amazon_eks + - docs/user-guide/deploy/deploy_to_amazon_ec2 + - label: Docker + items: + - docs/user-guide/deploy/deploy_to_docker + - docs/user-guide/deploy/deploy_to_docker/python + - docs/user-guide/deploy/deploy_to_docker/typescript + - docs/user-guide/deploy/deploy_to_kubernetes + - docs/user-guide/deploy/deploy_to_terraform - label: Safety & Security items: - docs/user-guide/safety-security/responsible-ai @@ -155,26 +180,6 @@ sidebar: items: - docs/user-guide/evals-sdk/how-to/experiment_management - docs/user-guide/evals-sdk/how-to/serialization - - label: Deploy - items: - - docs/user-guide/deploy/operating-agents-in-production - - label: Amazon Bedrock AgentCore - items: - - docs/user-guide/deploy/deploy_to_bedrock_agentcore - - docs/user-guide/deploy/deploy_to_bedrock_agentcore/python - - docs/user-guide/deploy/deploy_to_bedrock_agentcore/typescript - - docs/user-guide/deploy/deploy_to_aws_lambda - - docs/user-guide/deploy/deploy_to_aws_fargate - - docs/user-guide/deploy/deploy_to_aws_apprunner - - docs/user-guide/deploy/deploy_to_amazon_eks - - docs/user-guide/deploy/deploy_to_amazon_ec2 - - label: Docker - items: - - docs/user-guide/deploy/deploy_to_docker - - docs/user-guide/deploy/deploy_to_docker/python - - docs/user-guide/deploy/deploy_to_docker/typescript - - docs/user-guide/deploy/deploy_to_kubernetes - - docs/user-guide/deploy/deploy_to_terraform - docs/user-guide/versioning-and-support - label: Examples @@ -229,25 +234,23 @@ sidebar: - docs/community/tools/strands-teams - docs/community/tools/strands-telegram - docs/community/tools/strands-telegram-listener - - - label: Labs - items: - - docs/labs - - label: Projects + - label: Labs items: - - docs/labs/robots - - docs/labs/robots-sim - - docs/labs/ai-functions - - - label: Contribute ❤️ - items: - - docs/contribute - - label: Contribution Types + - docs/labs + - label: Projects + items: + - docs/labs/robots + - docs/labs/robots-sim + - docs/labs/ai-functions + - label: Contribute items: - - docs/contribute/contributing/core-sdk - - docs/contribute/contributing/documentation - - docs/contribute/contributing/feature-proposals - - docs/contribute/contributing/extensions + - docs/contribute + - label: Contribution Types + items: + - docs/contribute/contributing/core-sdk + - docs/contribute/contributing/documentation + - docs/contribute/contributing/feature-proposals + - docs/contribute/contributing/extensions github: sections: diff --git a/src/pages/index.astro b/src/pages/index.astro index df68429a9..8d9642670 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -80,7 +80,7 @@ const testimonials = await Promise.all( in a few lines of code
- Get Started + Get Started
diff --git a/src/pages/llms.txt.ts b/src/pages/llms.txt.ts index 844cbf03f..d7244842f 100644 --- a/src/pages/llms.txt.ts +++ b/src/pages/llms.txt.ts @@ -6,7 +6,7 @@ import { loadSidebarFromConfig, type StarlightSidebarItem } from '../sidebar' import path from 'node:path' // Sections to pull from sidebar (with their nav labels) -const SIDEBAR_SECTIONS = ['User Guide', 'Examples', 'Community'] +const SIDEBAR_SECTIONS = ['Docs', 'Examples', 'Community'] /** * Recursively extract links from sidebar items diff --git a/src/route-middleware.ts b/src/route-middleware.ts index fe84f9e42..eb660ba85 100644 --- a/src/route-middleware.ts +++ b/src/route-middleware.ts @@ -14,6 +14,7 @@ function isSidebarGroup(entry: SidebarEntry): entry is SidebarGroup { /** * Find which nav section the current page belongs to based on URL path. * Matches the most specific basePath (longest match wins). + * Also checks additionalBasePaths for nav items that span multiple sections. */ export function findCurrentNavSection(currentPath: string, links: NavLink[]): NavLink | undefined { let bestMatch: NavLink | undefined @@ -28,23 +29,36 @@ export function findCurrentNavSection(currentPath: string, links: NavLink[]): Na bestMatch = link bestMatchLength = basePath.length } + // Also check additionalBasePaths + if (link.additionalBasePaths) { + for (const additionalPath of link.additionalBasePaths) { + if (currentPath.startsWith(additionalPath) && additionalPath.length > bestMatchLength) { + bestMatch = link + bestMatchLength = additionalPath.length + } + } + } } return bestMatch } /** - * Filter sidebar entries to only include items matching a base path. + * Filter sidebar entries to only include items matching one or more base paths. * If the result is a single top-level group, unwrap it to return just its entries. */ -export function filterSidebarByBasePath(entries: SidebarEntry[], basePath: string): SidebarEntry[] { +export function filterSidebarByBasePath(entries: SidebarEntry[], basePath: string | string[]): SidebarEntry[] { + const basePaths = Array.isArray(basePath) ? basePath : [basePath] + + const matchesAnyBase = (href: string) => basePaths.some((bp) => href.startsWith(bp)) + const filtered = entries .map((entry) => { if (entry.type === 'link') { - return entry.href.startsWith(basePath) ? entry : null + return matchesAnyBase(entry.href) ? entry : null } if (entry.type === 'group') { - const filteredEntries = filterSidebarByBasePath(entry.entries, basePath) + const filteredEntries = filterSidebarByBasePath(entry.entries, basePaths) return filteredEntries.length > 0 ? { ...entry, entries: filteredEntries } : null } return null @@ -102,34 +116,8 @@ export const onRequest = defineRouteMiddleware(async (context) => { const currentPath = context.url.pathname const currentSlug = starlightRoute.id - // Check if we're on a Python API page - if (currentSlug.startsWith('docs/api/python')) { - const docs = await getCollection('docs') - const docInfos: DocInfo[] = docs.map((doc: { id: string; data: { title: unknown } }) => ({ - id: doc.id, - title: doc.data.title as string, - })) - - const pythonSidebar = buildPythonApiSidebar(docInfos, currentSlug) - - // Add index link at the top - pythonSidebar.unshift({ - type: 'link', - label: 'Overview', - href: pathWithBase('/docs/api/python/'), - isCurrent: currentSlug === 'docs/api/python', - badge: undefined, - attrs: {}, - }) - - const titlesByHref = await buildTitlesByHref() - starlightRoute.sidebar = pythonSidebar - starlightRoute.pagination = getPrevNextLinks(pythonSidebar, titlesByHref) - return - } - - // Check if we're on a TypeScript API page - if (currentSlug.startsWith('docs/api/typescript')) { + // Check if we're on an API page (Python or TypeScript) + if (currentSlug.startsWith('docs/api/python') || currentSlug.startsWith('docs/api/typescript')) { const docs = await getCollection('docs') const docInfos: DocInfo[] = docs.map((doc: { id: string; data: { title: unknown; category?: unknown } }) => ({ id: doc.id, @@ -137,21 +125,26 @@ export const onRequest = defineRouteMiddleware(async (context) => { category: doc.data.category as string | undefined, })) - const tsSidebar = buildTypeScriptApiSidebar(docInfos, currentSlug) + const isPython = currentSlug.startsWith('docs/api/python') + const apiSidebar = isPython + ? buildPythonApiSidebar(docInfos, currentSlug) + : buildTypeScriptApiSidebar(docInfos, currentSlug) // Add index link at the top - tsSidebar.unshift({ + const overviewHref = isPython ? '/docs/api/python/' : '/docs/api/typescript/' + const overviewSlug = isPython ? 'docs/api/python' : 'docs/api/typescript' + apiSidebar.unshift({ type: 'link', label: 'Overview', - href: pathWithBase('/docs/api/typescript/'), - isCurrent: currentSlug === 'docs/api/typescript', + href: pathWithBase(overviewHref), + isCurrent: currentSlug === overviewSlug, badge: undefined, attrs: {}, }) const titlesByHref = await buildTitlesByHref() - starlightRoute.sidebar = tsSidebar - starlightRoute.pagination = getPrevNextLinks(tsSidebar, titlesByHref) + starlightRoute.sidebar = apiSidebar + starlightRoute.pagination = getPrevNextLinks(apiSidebar, titlesByHref) return } @@ -164,19 +157,23 @@ export const onRequest = defineRouteMiddleware(async (context) => { return } + // Collect all base paths for this nav section (primary + additional) + const primaryBasePath = currentNav.basePath || currentNav.href + const allBasePaths = [primaryBasePath, ...(currentNav.additionalBasePaths || [])] + // Otherwise filter it down to the major section that we're in - const basePath = currentNav.basePath || currentNav.href - const filteredSidebar = filterSidebarByBasePath(sidebar, basePath) + const filteredSidebar = filterSidebarByBasePath(sidebar, allBasePaths) const expandedSidebar = expandFirstLevelGroups(filteredSidebar) starlightRoute.sidebar = expandedSidebar // Starlight pre-computes pagination from the full sidebar before our middleware runs. // Prune any prev/next links that fall outside the current nav section, then override // labels with actual page titles instead of sidebar nav labels. + const matchesAnyBase = (href: string) => allBasePaths.some((bp) => href.startsWith(bp)) const titlesByHref = await buildTitlesByHref() const { prev, next } = starlightRoute.pagination starlightRoute.pagination = { - prev: prev?.href.startsWith(basePath) ? { ...prev, label: titlesByHref.get(prev.href) ?? prev.label } : undefined, - next: next?.href.startsWith(basePath) ? { ...next, label: titlesByHref.get(next.href) ?? next.label } : undefined, + prev: prev && matchesAnyBase(prev.href) ? { ...prev, label: titlesByHref.get(prev.href) ?? prev.label } : undefined, + next: next && matchesAnyBase(next.href) ? { ...next, label: titlesByHref.get(next.href) ?? next.label } : undefined, } }) diff --git a/src/sidebar.ts b/src/sidebar.ts index bf73867c8..c26ecfe18 100644 --- a/src/sidebar.ts +++ b/src/sidebar.ts @@ -12,6 +12,7 @@ export type StarlightSidebarItem = interface NavConfigItem { label?: string items?: NavConfigItem[] + slug?: string // For labeled leaf items (e.g., { label: "Adding Tools", slug: "docs/user-guide/concepts/tools" }) } type NavConfigEntry = string | NavConfigItem @@ -82,6 +83,12 @@ function convertConfigItem(item: NavConfigEntry, ctx: ConvertContext): Starlight return { label: item.label, items: children } } + + // Object with label and slug (labeled leaf item) + if (item.label && item.slug) { + if (!contentExists(item.slug, ctx.contentDir)) return null + return { slug: item.slug, label: item.label } + } } return null diff --git a/test/sidebar.test.ts b/test/sidebar.test.ts index aa2536e8f..842ba719b 100644 --- a/test/sidebar.test.ts +++ b/test/sidebar.test.ts @@ -69,81 +69,81 @@ describe('Sidebar Generation from navigation.yml', () => { .filter((item): item is StarlightSidebarItem & { label: string } => 'label' in item) .map((item) => item.label) - expect(topLevelLabels).toContain('User Guide') + expect(topLevelLabels).toContain('Docs') expect(topLevelLabels).toContain('Examples') expect(topLevelLabels).toContain('Community') - expect(topLevelLabels).toContain('Labs') - expect(topLevelLabels).toContain('Contribute ❤️') }) it('should have collapsed groups at depth >= 1', () => { const sidebar = loadSidebarFromConfig(pathToNavigationYml) - // Find the User Guide section - const userGuide = sidebar.find( + // Find the Docs section + const docs = sidebar.find( (item): item is StarlightSidebarItem & { label: string; items: StarlightSidebarItem[] } => - 'label' in item && item.label === 'User Guide' + 'label' in item && item.label === 'Docs' ) - expect(userGuide).toBeDefined() - if (userGuide) { + expect(docs).toBeDefined() + if (docs) { // Top level should not be collapsed - expect(userGuide).not.toHaveProperty('collapsed') + expect(docs).not.toHaveProperty('collapsed') - // Find a nested group (like "Quickstart") - const quickstart = userGuide.items.find( - (item): item is StarlightSidebarItem & { label: string } => 'label' in item && item.label === 'Quickstart' + // Find a nested group (like "Get Started") + const getStarted = docs.items.find( + (item): item is StarlightSidebarItem & { label: string } => 'label' in item && item.label === 'Get Started' ) // Nested groups should be collapsed - expect(quickstart).toHaveProperty('collapsed', true) + expect(getStarted).toHaveProperty('collapsed', true) } }) - it('should have slugs without labels for file items', () => { + it('should support both labeled and unlabeled leaf items', () => { const sidebar = loadSidebarFromConfig(pathToNavigationYml) - // Find a leaf item (internal link) and verify it has slug but no label - function findLeafItem(items: StarlightSidebarItem[]): StarlightSidebarItem | null { + // Collect all leaf items + function findLeafItems(items: StarlightSidebarItem[]): StarlightSidebarItem[] { + const leaves: StarlightSidebarItem[] = [] for (const item of items) { if ('slug' in item && !('items' in item)) { - return item + leaves.push(item) } if ('items' in item) { - const found = findLeafItem(item.items as StarlightSidebarItem[]) - if (found) return found + leaves.push(...findLeafItems(item.items as StarlightSidebarItem[])) } } - return null + return leaves } - const leafItem = findLeafItem(sidebar) - expect(leafItem).toBeDefined() - expect(leafItem).toHaveProperty('slug') - expect(leafItem).not.toHaveProperty('label') + const leaves = findLeafItems(sidebar) + expect(leaves.length).toBeGreaterThan(0) + + // Some leaves should have labels (Build section items) + const labeled = leaves.filter((item) => 'label' in item) + expect(labeled.length).toBeGreaterThan(0) + + // Some leaves should not have labels (plain slug items) + const unlabeled = leaves.filter((item) => !('label' in item)) + expect(unlabeled.length).toBeGreaterThan(0) }) - it('should handle external links in sidebar', () => { + it('should include Labs and Contribute under Community', () => { const sidebar = loadSidebarFromConfig(pathToNavigationYml) - // Find the Contribute section which has an external link - const contribute = sidebar.find( - (item): item is StarlightSidebarItem & { label: string } => - 'label' in item && item.label === 'Contribute ❤️' + // Find the Community section + const community = sidebar.find( + (item): item is StarlightSidebarItem & { label: string; items: StarlightSidebarItem[] } => + 'label' in item && item.label === 'Community' ) - expect(contribute).toBeDefined() - if (contribute && 'items' in contribute) { - // Look for external links in the items - const externalLink = (contribute.items as StarlightSidebarItem[]).find( - (item): item is StarlightSidebarItem & { link: string } => 'link' in item && item.link?.startsWith('http') - ) + expect(community).toBeDefined() + if (community) { + const subLabels = community.items + .filter((item): item is StarlightSidebarItem & { label: string } => 'label' in item) + .map((item) => item.label) - // If there's an external link, it should have the right attributes - if (externalLink) { - expect(externalLink).toHaveProperty('attrs') - expect(externalLink.attrs).toHaveProperty('target', '_blank') - } + expect(subLabels).toContain('Labs') + expect(subLabels).toContain('Contribute') } }) })