feat: migrate sidebar and navbar config to navigation.yml (#617)

Co-authored-by: Strands Agent <217235299+strands-agent@users.noreply.github.com>
Co-authored-by: Mackenzie Zastrow <zastrowm@users.noreply.github.com>
This commit is contained in:
Mackenzie Zastrow
2026-03-09 10:39:33 -04:00
committed by GitHub
co-authored by Strands Agent Mackenzie Zastrow
parent 79c1ac9a7d
commit 5e3f97c684
20 changed files with 478 additions and 1702 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
## Content Location
`src/content/docs` is a symlink to `../../docs`. Starlight expects content in `src/content/docs/`, but we haven't fully migrated from the mkdocs structure yet. The symlink lets both systems work during the transition.
`src/content/docs` is a symlink to `../../docs`. Starlight expects content in `src/content/docs/`, providing a consistent structure for documentation.
## Code Snippets from TypeScript Files
+2 -12
View File
@@ -1,18 +1,8 @@
# CMS Migration TODO
## Before launch:
- [ ] Remove Vite SSR workaround for zod in `astro.config.mjs` once CMS build is separated from TS verification (see https://github.com/withastro/astro/issues/14117)
- [X] Fix relative links to pages (e.g., `../some-page.md` style links need to be converted to Starlight-compatible paths)
- [X] Add API documentation generation/integration for Python and TypeScript SDKs
- [ ] Fix type-checking
- [ ] Look into markdown
- [X] Add header links to Python/TypeScript method sections (docs/api/python/strands.agent.agent/)
- [ ] Migrate all files and remove conversion scripts
- [ ] Remove `<name>Data` special-case in `scripts/api-generation-typescript.ts` once typedoc fixes the prose or we find a better general solution
## After Launch
- [ ] Remove Vite SSR workaround for zod in `astro.config.mjs` once CMS build is separated from TS verification (see https://github.com/withastro/astro/issues/14117)
- [ ] Remove `<name>Data` special-case in `scripts/api-generation-typescript.ts` once typedoc fixes the prose or we find a better general solution
- [ ] Move asset files to proper location (currently in `docs/assets/`, should be in `src/content/docs/assets/`)
- [ ] Migrate sidebar from mkdocs.yml to something astro specific (and combine with the navbar?)
- [ ] Remove symlink at `src/content/docs` → `../../docs` and move content to actual location
- [ ] Inline TypeScript code examples directly in markdown and verify via separate type-checking step (e.g., `tsc` on extracted code blocks) instead of snippet includes
- [ ] Update astro-auto-import once https://github.com/delucis/astro-auto-import/pull/110 is merged
+1 -1
View File
@@ -26,7 +26,7 @@ npm run build # generates static site
### Writing Docs
- Content lives in `docs/` as standard Markdown files
- Navigation structure is defined in `mkdocs.yml`
- Navigation structure is defined in `src/config/navigation.yml`
- Use `<Tabs>` / `<Tab label="...">` for Python/TypeScript code tabs (auto-imported)
- Use `--8<-- "path/to/file.ts:snippet_name"` to pull in code snippets from external files
- Relative file links (e.g. `../tools/index.md`) resolve automatically — no need to use slugs
+1 -1
View File
@@ -63,7 +63,7 @@ npm run dev
### Writing Documentation
Documentation lives in `docs/` as Markdown files. The site structure is driven by `mkdocs.yml` (navigation) and rendered by Astro at build time.
Documentation lives in `docs/` as Markdown files. The site structure is driven by `src/config/navigation.yml` (navigation) and rendered by Astro at build time.
- Pages are written in standard Markdown — no Astro-specific syntax needed for content edits
- Use `<Tabs>` / `<Tab label="...">` for language-switching code blocks (auto-imported, no import needed)
+10 -45
View File
@@ -10,15 +10,11 @@ We're using [Astro](https://astro.build/) with the [Starlight](https://starlight
### 1. Sidebar Generation (`src/sidebar.ts`)
**What it does:** Reads the navigation structure from `mkdocs.yml` and converts it to Starlight's sidebar format. Also extracts sidebar labels and badges from nav entries.
**What it does:** Reads the navigation structure from `src/config/navigation.yml` and converts it to Starlight's sidebar format.
**Why:** Starlight can auto-generate sidebars from the file structure, but we have a specific navigation layout defined in `mkdocs.yml` that we want to preserve. This ensures consistency during the migration from MkDocs to Astro.
**Why:** Starlight can auto-generate sidebars from the file structure, but we have a specific navigation layout defined in `navigation.yml` that we want to preserve. The config file also contains navbar and GitHub dropdown configuration.
**Label and badge extraction:** The `getSidebarLabels()` function parses nav entries with `<sup>` tags (e.g., `AWS Lambda<sup> new</sup>`) and extracts:
- `label`: The clean text without HTML tags (e.g., "AWS Lambda")
- `badge`: The badge text from `<sup>` tags (e.g., "new", "community")
This data is used by `scripts/update-docs.ts` to generate sidebar frontmatter during the migration process.
**Badges:** Badges (like "new", "community", "experimental") come from page frontmatter, not the navigation config. This allows page authors to control badges directly.
### 2. Route Middleware (`src/route-middleware.ts`)
@@ -122,12 +118,12 @@ The collection base is `src/content` (not `src/content/docs`), so all doc slugs
The main config ties everything together:
```javascript
import { loadSidebarFromMkdocs } from "./src/sidebar.ts"
import { loadSidebarFromConfig } from "./src/sidebar.ts"
import remarkMkdocsSnippets from './src/plugins/remark-mkdocs-snippets.ts'
import AutoImport from 'astro-auto-import'
const sidebar = loadSidebarFromMkdocs(
path.resolve('./mkdocs.yml'),
const sidebar = loadSidebarFromConfig(
path.resolve('./src/config/navigation.yml'),
path.resolve('./src/content') // base is src/content, not src/content/docs
)
@@ -165,20 +161,6 @@ Notable config details:
- `themeCssSelector` on Expressive Code makes code block themes follow Starlight's `[data-theme]` attribute rather than the browser's `prefers-color-scheme`, keeping syntax highlighting in sync with the site's theme toggle.
- `processedDirs` tells Starlight to run its rehype plugins (e.g. heading anchor links) on the real resolved paths of the API docs symlinks.
## Temporary Migration Script (`scripts/update-docs.ts`)
**What it does:** Converts documentation files from MkDocs markdown format to Astro/Starlight markdown format.
**Why:** MkDocs and Astro use different markdown conventions. Rather than updating Astro's parser to support MkDocs syntax, we convert files to what Astro expects. This runs at build time because the main branch (still in MkDocs format) is a moving target until migration is complete.
**Current approach:** Source control keeps files in MkDocs format. The script runs at build time to transform them. Once migration is complete, we'll do a final conversion, remove the script, and commit the transformed files directly.
For detailed information about what transformations the script performs (and what's planned), see [`scripts/update-docs.md`](scripts/update-docs.md).
Notable behaviors:
- Collapsible MkDocs admonitions (`???` and `???+`) are converted alongside standard `!!!` admonitions.
- Bidirectional-streaming pages are excluded from receiving the experimental sidebar badge (the badge is already present via other means for those pages).
## Custom Frontmatter Fields
The documentation extends Starlight's default schema with custom fields that automatically render contextual banners at the top of pages.
@@ -243,14 +225,7 @@ sidebar:
Available variants: `note`, `tip`, `caution`, `danger`, `success`, `default`
**Automatic extraction:** During migration, `scripts/update-docs.ts` automatically generates sidebar frontmatter from `mkdocs.yml` nav entries. Entries like `AWS Lambda<sup> new</sup>` become:
```yaml
sidebar:
label: "AWS Lambda"
badge:
text: New
variant: note
```
**Badge sources:** Badges like "Experimental" or "Community" are determined from page frontmatter. Add a `sidebar.badge` field to a page's frontmatter to display a badge in the sidebar.
## MDX Components
@@ -682,21 +657,11 @@ These files handle converting old MkDocs-style API reference links to the new `@
### Migration Scripts
These scripts transform MkDocs markdown to Astro-compatible format at build time:
These scripts assist with documentation maintenance:
- `scripts/update-docs.ts` - Main transformation script (converts admonitions, tabs, API links, etc.)
- `scripts/update-quickstart.ts` - Quickstart-specific transformations
- `test/update-docs.test.ts` - Tests for the update-docs transformations
### When to Delete
Once the migration is complete and all documentation is committed in Astro format:
1. Run `npm run docs:update` one final time to apply all transformations
2. Commit the transformed files directly (no longer keeping MkDocs format in source control)
3. Delete the files listed above
4. Remove the `docs:update` and `docs:revert` scripts from `package.json`
5. Update this README to remove references to the migration process
- `scripts/update-language-index.ts` - Updates language index pages
- `test/update-docs.test.ts` - Tests for API link conversion utilities
## URL Redirects (Old MkDocs URLs → New CMS URLs)
+4 -4
View File
@@ -5,16 +5,16 @@ import path from 'node:path'
import remarkMkdocsSnippets from './src/plugins/remark-mkdocs-snippets.ts'
import sdkSetupPlugin from './src/plugins/vite-plugin-sdk-setup.ts'
import { loadSidebarFromMkdocs } from "./src/sidebar.ts"
import { loadSidebarFromConfig } from "./src/sidebar.ts"
import AutoImport from './src/plugins/astro-auto-import.ts'
import astroExpressiveCode from "astro-expressive-code"
import mdx from '@astrojs/mdx';
import astroBrokenLinksChecker from './scripts/astro-broken-links-checker-index.js';
// Generate sidebar from mkdocs nav (validates against existing content files)
// Generate sidebar from navigation.yml config (validates against existing content files)
// Top-level groups will be rendered as tabs by the custom Sidebar component
const sidebar = loadSidebarFromMkdocs(
path.resolve('./mkdocs.yml'),
const sidebar = loadSidebarFromConfig(
path.resolve('./src/config/navigation.yml'),
path.resolve('./src/content')
)
-340
View File
@@ -1,340 +0,0 @@
site_name: Strands Agents SDK
site_description: Documentation for Strands Agents, a simple-to-use, code-first, lightweight library for building AI agents
site_dir: site
site_url: https://strandsagents.com
repo_url: https://github.com/strands-agents/sdk-python
edit_uri: https://github.com/strands-agents/docs/edit/main/docs
theme:
name: material
custom_dir: overrides
logo: assets/logo-light.svg # Safari doesn't support <style> tags inside SVGs so we need to a light and a dark SVG
logo_dark: assets/logo-dark.svg # Safari doesn't support <style> tags inside SVGs so we need to a light and a dark SVG
favicon: assets/logo-auto.svg
favicon_png: assets/logo-light.png # Safari doesn't support SVG favicons
palette:
# Palette toggle for light mode
- media: "(prefers-color-scheme: light)"
primary: custom
scheme: default
toggle:
icon: material/brightness-7
name: Switch to dark mode
# Palette toggle for dark mode
- media: "(prefers-color-scheme: dark)"
primary: custom
scheme: slate
toggle:
icon: material/brightness-4
name: Switch to light mode
icon:
admonition:
code: material/code-json
features:
- content.action.edit
- content.code.copy
- content.tabs.link
- content.code.select
- navigation.indexes
- navigation.instant
- navigation.instant.prefetch
- navigation.instant.progress
- navigation.tabs
- navigation.tabs.sticky
- navigation.sections
- navigation.top
- search.highlight
- content.code.copy
markdown_extensions:
- admonition
- codehilite
- pymdownx.highlight
- pymdownx.tabbed:
alternate_style: true
slugify: !!python/object/apply:pymdownx.slugs.slugify
kwds:
case: lower
- pymdownx.details
- pymdownx.emoji:
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
- pymdownx.snippets:
base_path: ["docs"]
check_paths: true
dedent_subsections: true
- pymdownx.superfences:
custom_fences:
- name: mermaid
class: mermaid
format: !!python/name:pymdownx.superfences.fence_code_format
- tables
- toc:
title: On this page
permalink: true
extra_css:
- stylesheets/extra.css
extra_javascript:
# Workaround for site_url breaking mermaid rendering; see the following for more info:
# https://github.com/squidfunk/mkdocs-material/issues/3742#issuecomment-1076068038
- https://unpkg.com/mermaid@11/dist/mermaid.min.js
- assets/auto-redirect.js
nav:
- User Guide:
- Welcome: README.md
- Quickstart:
- Getting Started: user-guide/quickstart/overview.md
- Python: user-guide/quickstart/python.md
- TypeScript: user-guide/quickstart/typescript.md
- Concepts:
- Agents:
- Agent Loop: user-guide/concepts/agents/agent-loop.md
- State: user-guide/concepts/agents/state.md
- Session Management: user-guide/concepts/agents/session-management.md
- Prompts: user-guide/concepts/agents/prompts.md
- Retry Strategies: user-guide/concepts/agents/retry-strategies.md
- Hooks: user-guide/concepts/agents/hooks.md
- Structured Output: user-guide/concepts/agents/structured-output.md
- Conversation Management: user-guide/concepts/agents/conversation-management.md
- Tools:
- Overview: user-guide/concepts/tools/index.md
- Creating Custom Tools: user-guide/concepts/tools/custom-tools.md
- Model Context Protocol (MCP): user-guide/concepts/tools/mcp-tools.md
- Executors: user-guide/concepts/tools/executors.md
- Community Tools Package: user-guide/concepts/tools/community-tools-package.md
- Model Providers:
- Overview: user-guide/concepts/model-providers/index.md
- Amazon Bedrock: user-guide/concepts/model-providers/amazon-bedrock.md
- Amazon Nova: user-guide/concepts/model-providers/amazon-nova.md
- Anthropic: user-guide/concepts/model-providers/anthropic.md
- Gemini: user-guide/concepts/model-providers/gemini.md
- LiteLLM: user-guide/concepts/model-providers/litellm.md
- llama.cpp: user-guide/concepts/model-providers/llamacpp.md
- LlamaAPI: user-guide/concepts/model-providers/llamaapi.md
- MistralAI: user-guide/concepts/model-providers/mistral.md
- Ollama: user-guide/concepts/model-providers/ollama.md
- OpenAI: user-guide/concepts/model-providers/openai.md
- SageMaker: user-guide/concepts/model-providers/sagemaker.md
- Writer: user-guide/concepts/model-providers/writer.md
- Custom Providers: user-guide/concepts/model-providers/custom_model_provider.md
- Cohere<sup> community</sup>: user-guide/concepts/model-providers/cohere.md
- CLOVA Studio<sup> community</sup>: user-guide/concepts/model-providers/clova-studio.md
- FireworksAI<sup> community</sup>: user-guide/concepts/model-providers/fireworksai.md
- Nebius Token Factory<sup> community</sup>: user-guide/concepts/model-providers/nebius-token-factory.md
- xAI<sup> community</sup>: user-guide/concepts/model-providers/xai.md
- Streaming:
- Overview: user-guide/concepts/streaming/index.md
- Async Iterators: user-guide/concepts/streaming/async-iterators.md
- Callback Handlers: user-guide/concepts/streaming/callback-handlers.md
- Multi-agent:
- Agent2Agent (A2A): user-guide/concepts/multi-agent/agent-to-agent.md
- Agents as Tools: user-guide/concepts/multi-agent/agents-as-tools.md
- Swarm: user-guide/concepts/multi-agent/swarm.md
- Graph: user-guide/concepts/multi-agent/graph.md
- Workflow: user-guide/concepts/multi-agent/workflow.md
- Multi-agent Patterns: user-guide/concepts/multi-agent/multi-agent-patterns.md
- Interrupts: user-guide/concepts/interrupts.md
- Bidirectional Streaming:
- Quickstart: user-guide/concepts/bidirectional-streaming/quickstart.md
- BidiAgent: user-guide/concepts/bidirectional-streaming/agent.md
- Models:
- Nova Sonic: user-guide/concepts/bidirectional-streaming/models/nova_sonic.md
- Gemini Live: user-guide/concepts/bidirectional-streaming/models/gemini_live.md
- OpenAI Realtime: user-guide/concepts/bidirectional-streaming/models/openai_realtime.md
- IO: user-guide/concepts/bidirectional-streaming/io.md
- Events: user-guide/concepts/bidirectional-streaming/events.md
- Interruptions: user-guide/concepts/bidirectional-streaming/interruption.md
- Hooks: user-guide/concepts/bidirectional-streaming/hooks.md
- Session Management: user-guide/concepts/bidirectional-streaming/session-management.md
- Experimental:
- AgentConfig: user-guide/concepts/experimental/agent-config.md
- Steering: user-guide/concepts/experimental/steering.md
- Safety & Security:
- Responsible AI: user-guide/safety-security/responsible-ai.md
- Guardrails: user-guide/safety-security/guardrails.md
- Prompt Engineering: user-guide/safety-security/prompt-engineering.md
- PII Redaction: user-guide/safety-security/pii-redaction.md
- Observability & Debugging:
- Observability: user-guide/observability-evaluation/observability.md
- Metrics: user-guide/observability-evaluation/metrics.md
- Traces: user-guide/observability-evaluation/traces.md
- Logs: user-guide/observability-evaluation/logs.md
- Strands Evals SDK:
- Getting Started: user-guide/evals-sdk/quickstart.md
- Eval SOP: user-guide/evals-sdk/eval-sop.md
- Evaluators:
- Overview: user-guide/evals-sdk/evaluators/index.md
- Output: user-guide/evals-sdk/evaluators/output_evaluator.md
- Trajectory: user-guide/evals-sdk/evaluators/trajectory_evaluator.md
- Interactions: user-guide/evals-sdk/evaluators/interactions_evaluator.md
- Helpfulness: user-guide/evals-sdk/evaluators/helpfulness_evaluator.md
- Faithfulness: user-guide/evals-sdk/evaluators/faithfulness_evaluator.md
- Goal Success Rate: user-guide/evals-sdk/evaluators/goal_success_rate_evaluator.md
- Tool Selection Accuracy: user-guide/evals-sdk/evaluators/tool_selection_evaluator.md
- Tool Parameter Accuracy: user-guide/evals-sdk/evaluators/tool_parameter_evaluator.md
- Custom: user-guide/evals-sdk/evaluators/custom_evaluator.md
- Experiment Generator: user-guide/evals-sdk/experiment_generator.md
- Simulators:
- Overview: user-guide/evals-sdk/simulators/index.md
- User Simulation: user-guide/evals-sdk/simulators/user_simulation.md
- How-To Guides:
- Experiment Management: user-guide/evals-sdk/how-to/experiment_management.md
- Serialization: user-guide/evals-sdk/how-to/serialization.md
- Deploy:
- Operating Agents in Production: user-guide/deploy/operating-agents-in-production.md
- Amazon Bedrock AgentCore:
- Overview: user-guide/deploy/deploy_to_bedrock_agentcore/index.md
- Python: user-guide/deploy/deploy_to_bedrock_agentcore/python.md
- TypeScript: user-guide/deploy/deploy_to_bedrock_agentcore/typescript.md
- AWS Lambda<sup> new</sup>: user-guide/deploy/deploy_to_aws_lambda.md
- AWS Fargate: user-guide/deploy/deploy_to_aws_fargate.md
- AWS App Runner: user-guide/deploy/deploy_to_aws_apprunner.md
- Amazon EKS: user-guide/deploy/deploy_to_amazon_eks.md
- Amazon EC2: user-guide/deploy/deploy_to_amazon_ec2.md
- Docker:
- Overview: user-guide/deploy/deploy_to_docker/index.md
- Python: user-guide/deploy/deploy_to_docker/python.md
- TypeScript: user-guide/deploy/deploy_to_docker/typescript.md
- Kubernetes: user-guide/deploy/deploy_to_kubernetes.md
- Terraform: user-guide/deploy/deploy_to_terraform.md
- Versioning & Support: user-guide/versioning-and-support.md
- Examples:
- Overview: examples/README.md
- CLI Reference Agent Implementation: examples/python/cli-reference-agent.md
- Weather Forecaster: examples/python/weather_forecaster.md
- Memory Agent: examples/python/memory_agent.md
- File Operations: examples/python/file_operations.md
- Agents Workflows: examples/python/agents_workflows.md
- Knowledge-Base Workflow: examples/python/knowledge_base_agent.md
- Structured Output: examples/python/structured_output.md
- Multi Agents: examples/python/multi_agent_example/multi_agent_example.md
- Cyclic Graph: examples/python/graph_loops_example.md
- Meta Tooling: examples/python/meta_tooling.md
- MCP: examples/python/mcp_calculator.md
- Multi-modal: examples/python/multimodal.md
- Community:
- Community Catalog: community/community-packages.md
- Get Featured: community/get-featured.md
- Integrations:
- AG-UI: community/integrations/ag-ui.md
- Model Providers:
- Cohere: community/model-providers/cohere.md
- CLOVA Studio: community/model-providers/clova-studio.md
- Fireworks AI: community/model-providers/fireworksai.md
- Nebius Token Factory: community/model-providers/nebius-token-factory.md
- NVIDIA NIM: community/model-providers/nvidia-nim.md
- SGLang: community/model-providers/sglang.md
- vLLM: community/model-providers/vllm.md
- MLX: community/model-providers/mlx.md
- xAI: community/model-providers/xai.md
- Session Managers:
- Amazon AgentCore Memory: community/session-managers/agentcore-memory.md
- Valkey/Redis: community/session-managers/strands-valkey-session-manager.md
- Tool Protocols:
- UTCP: community/tools/utcp.md
- Tools:
- deepgram: community/tools/strands-deepgram.md
- hubspot: community/tools/strands-hubspot.md
- teams: community/tools/strands-teams.md
- telegram: community/tools/strands-telegram.md
- telegram-listener: community/tools/strands-telegram-listener.md
- Labs:
- Overview: labs/index.md
- Projects:
- Robots: labs/robots.md
- Robots Sim: labs/robots-sim.md
- AI Functions: labs/ai-functions.md
- Contribute ❤️:
- Overview: contribute/index.md
- Contribution Types:
- SDK: contribute/contributing/core-sdk.md
- Documentation: contribute/contributing/documentation.md
- Feature Proposals: contribute/contributing/feature-proposals.md
- Extensions: contribute/contributing/extensions.md
- Python API: []
- TypeScript API: api-reference/typescript/index.html
exclude_docs: |
node_modules
.venv
_dependencies
!.lycheeignore
plugins:
- search
- privacy
- macros:
module_name: macros
- mike:
alias_type: symlink
canonical_version: latest
- mkdocstrings:
handlers:
python:
options:
docstring_style: google
show_root_heading: true
show_source: true
- llmstxt:
full_output: llms-full.txt
sections:
User Guide:
- README.md
- user-guide/**/*.md
Community:
- community/**/*.md
Examples:
- examples/**/*.md
Python API:
- docs/api-reference/python/**/*.md
TypeScript API:
- docs/api-reference/typescript/*.html
hooks:
- build-ts-docs.py
- build-py-docs.py
extra:
social:
- icon: fontawesome/brands/github
version:
provider: mike
# Variables
docs_repo: https://github.com/strands-agents/docs/tree/main
sdk_pypi: https://pypi.org/project/strands-agents/
sdk_repo: https://github.com/strands-agents/sdk-python/blob/main
py_sdk_repo_home: https://github.com/strands-agents/sdk-python/blob/main
ts_sdk_repo_home: https://github.com/strands-agents/sdk-typescript/blob/main
tools_pypi: https://pypi.org/project/strands-agents-tools/
tools_repo: https://github.com/strands-agents/tools/blob/main
tools_repo_home: https://github.com/strands-agents/tools
agent_builder_pypi: https://pypi.org/project/strands-agents-builder/
agent_builder_repo_home: https://github.com/strands-agents/agent-builder
link_strands_tools: "[`strands-agents-tools`](https://github.com/strands-agents/tools)"
link_strands_builder: "[`strands-agents-builder`](https://github.com/strands-agents/agent-builder)"
community_contribution_banner: |
!!! info "Community Contribution"
This is a community-maintained package that is not owned or supported by the Strands team. Validate and review
the package before using it in your project.
Have your own integration? [We'd love to add it here too!](https://github.com/strands-agents/docs/issues/new?assignees=&labels=enhancement&projects=&template=content_addition.yml&title=%5BContent+Addition%5D%3A+)
validation:
nav:
omitted_files: info
not_found: warn
absolute_links: warn
links:
not_found: warn
anchors: warn
absolute_links: warn
unrecognized_links: warn
-2
View File
@@ -14,8 +14,6 @@
"clean": "rm -rf .build && rm -rf .astro",
"preview": "astro preview",
"cms:build": "npm run build:all",
"docs:update": "tsx scripts/update-docs.ts",
"docs:revert": "git checkout HEAD -- docs/ && git clean -d -f src/content/docs",
"sdk:clone": "tsx scripts/clone-sdks.ts",
"sdk:generate:py": "command -v uv >/dev/null 2>&1 && uv run scripts/api-generation-python.py || (pip install pydoc-markdown>=4.8.2 && python scripts/api-generation-python.py)",
"sdk:generate:ts": "tsx scripts/api-generation-typescript.ts",
+4
View File
@@ -0,0 +1,4 @@
<svg width="290" height="463" viewBox="0 0 290 463" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M97.2902 52.7884C85.0674 49.1667 72.2234 56.1389 68.6017 68.3616C64.9801 80.5843 71.9524 93.4283 84.1749 97.0501L235.117 139.775C245.223 142.769 246.357 156.628 236.874 161.226L32.546 260.291C-14.9439 283.316 -9.16107 352.74 41.4835 367.591L189.551 411.009L190.125 411.169C202.183 414.376 214.665 407.396 218.196 395.355C221.784 383.122 214.774 370.296 202.541 366.709L54.4738 323.291C44.3447 320.321 43.1879 306.436 52.6857 301.831L257.014 202.766C304.432 179.776 298.758 110.483 248.233 95.512L97.2902 52.7884Z" fill="#989898"/>
<path d="M259.147 0.981812C271.389 -2.57498 284.197 4.46571 287.754 16.7074C291.311 28.9492 284.27 41.757 272.028 45.3138L71.1727 103.671C40.7142 112.521 37.1976 154.262 65.7459 168.083L241.343 253.093C307.872 285.302 299.794 382.546 228.862 403.336L30.4041 461.502C18.1707 465.088 5.34708 458.078 1.76153 445.844C-1.8239 433.611 5.18637 420.787 17.4197 417.202L215.878 359.035C246.277 350.125 249.739 308.449 221.226 294.645L45.6297 209.635C-20.9834 177.386 -12.7772 79.9893 58.2928 59.3402L259.147 0.981812Z" fill="#00FF77"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

-29
View File
@@ -1,29 +0,0 @@
#### Markdown Conversion
`./update-docs.ts` is a script to convert from MkDocs markdown format to Astro/Starlight markdown.
**Why convert?** Markdown as a spec is very loose and everyone's markdown is different. All of the features we use in our [current CMS MkDocs](https://www.mkdocs.org/getting-started/) are not available in the new [CMS Astro](https://starlight.astro.build/). While we *could* update the Astro markdown parser to support the same features, it makes more sense to do a conversion to what Astro expects instead, allowing us to move faster and leverage more Astro features going forward.
**Why not convert everything now?** Until we fully migrate to the new CMS, the main branch is a moving target. Converting all files upfront would mean we'd lose changes being made on main, or we'd need to continuously re-sync and re-convert as updates come in. Instead, we run the conversion as part of the build process.
**Current approach:** Source control keeps files in MkDocs format. The conversion script runs at build time to transform them for Astro. Once we complete the migration to the new CMS, we'll do a final conversion, remove the script, and commit the transformed files directly to source.
#### What Gets Converted
The conversion script now handles all required transformations:
- **MkDocs variables** (`{{ var }}`) → Replaced with actual values from `MKDOCS_VARIABLES` map
- **MkDocs admonitions** (`!!! type "Title"`) → Astro asides (`:::type[Title]`)
- **MkDocs tabs** (`=== "Label"`) → `<Tabs>`/`<Tab>` components
- **HTML comments** (`<!-- -->`) → JSX comments (`{/* */}`) for MDX compatibility
- **HTML `<br>` tags** → Self-closing `<br />` for MDX compatibility
- **File extensions** (`.md`) → Renamed to `.mdx`
- **Frontmatter** → `title` extracted from H1 heading (then H1 removed from content)
- **Macro replacements**:
- `{{ ts_not_supported() }}` → Astro aside note
- `{{ ts_not_supported_code() }}` → TypeScript tab with comment
- `{{ experimental_feature_warning() }}` → Removed (tracked via `experimental: true` frontmatter)
- `{{ community_contribution_banner }}` → Removed (tracked via `community: true` frontmatter)
- **Language support blocks** → Removed (tracked via `languages: Python` frontmatter)
- **Sidebar badges** → Added for community and experimental content
- **`[Experimental]` in titles** → Stripped and converted to `experimental: true` frontmatter
-951
View File
@@ -1,951 +0,0 @@
import { readdir, readFile, writeFile, mkdir, unlink } from "fs/promises";
import { join, dirname } from "path";
import { updateQuickstart } from "./update-quickstart.js";
import { updateLanguageIndexFiles } from "./update-language-index.js";
import { getCommunityLabeledFiles, getSidebarLabels, type SidebarInfo } from "../src/sidebar.js";
import { convertApiLink, isOldApiLink } from "../src/util/api-link-converter.js";
const DOCS_DIR = "docs";
const OUTPUT_DIR = "src/content/docs";
const MKDOCS_PATH = "mkdocs.yml";
const INFO_BLOCK_PATTERN = '!!! info "Language Support"';
const INFO_BLOCK_CONTENT = " This provider is only supported in Python.";
const COMMUNITY_BANNER = "{{ community_contribution_banner }}";
const SKIP_FILES: string[] = [];
// Skip index files in examples directory (they're not included in the content collection)
const SKIP_PATTERNS = [/examples\/.*\/index\.md$/];
// Files that need explicit titles because they don't have H1 headings
const EXPLICIT_TITLES: Record<string, string> = {
"user-guide/quickstart.md": "Quickstart",
"user-guide/quickstart/python.md": "Python Quickstart",
// Redirect pages (have <auto-redirect /> but no H1)
"user-guide/concepts/model-providers/clova-studio.md": "Clova Studio",
"user-guide/concepts/model-providers/cohere.md": "Cohere",
"user-guide/concepts/model-providers/fireworksai.md": "Fireworks AI",
"user-guide/concepts/model-providers/xai.md": "xAI",
"user-guide/concepts/model-providers/nebius-token-factory.md": "Nebius Token Factory",
// Labs pages (don't have H1 headings in source)
"labs/index.md": "Strands Labs",
"labs/robots.md": "Robots",
"labs/robots-sim.md": "Robots Sim",
"labs/ai-functions.md": "AI Functions",
// Contribute pages
"contribute/index.md": "Contribute",
"contribute/contributing/core-sdk.md": "Contributing to the SDK",
"contribute/contributing/documentation.md": "Contributing to Documentation",
"contribute/contributing/extensions.md": "Publishing Extensions",
"contribute/contributing/feature-proposals.md": "Feature Proposals",
};
// MkDocs extra variables from mkdocs.yml
const MKDOCS_VARIABLES: Record<string, string> = {
docs_repo: "https://github.com/strands-agents/docs/tree/main",
sdk_pypi: "https://pypi.org/project/strands-agents/",
sdk_repo: "https://github.com/strands-agents/sdk-python/blob/main",
py_sdk_repo_home: "https://github.com/strands-agents/sdk-python/blob/main",
ts_sdk_repo_home: "https://github.com/strands-agents/sdk-typescript/blob/main",
tools_pypi: "https://pypi.org/project/strands-agents-tools/",
tools_repo: "https://github.com/strands-agents/tools/blob/main",
tools_repo_home: "https://github.com/strands-agents/tools",
agent_builder_pypi: "https://pypi.org/project/strands-agents-builder/",
agent_builder_repo_home: "https://github.com/strands-agents/agent-builder",
link_strands_tools: "[`strands-agents-tools`](https://github.com/strands-agents/tools)",
link_strands_builder: "[`strands-agents-builder`](https://github.com/strands-agents/agent-builder)",
};
// Default messages for macros
const DEFAULT_TS_NOT_SUPPORTED = "This feature is not supported in TypeScript.";
const DEFAULT_TS_NOT_SUPPORTED_CODE = "Not supported in TypeScript";
const DEFAULT_EXPERIMENTAL_WARNING =
"This feature is experimental and may change in future versions. Use with caution in production environments.";
async function getAllMarkdownFiles(dir: string): Promise<string[]> {
const files: string[] = [];
async function walk(currentDir: string) {
const entries = await readdir(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(currentDir, entry.name);
if (entry.isDirectory()) {
await walk(fullPath);
} else if (entry.name.endsWith(".md")) {
files.push(fullPath);
}
}
}
await walk(dir);
return files;
}
async function getAllTypeScriptFiles(dir: string): Promise<string[]> {
const files: string[] = [];
async function walk(currentDir: string) {
const entries = await readdir(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(currentDir, entry.name);
if (entry.isDirectory()) {
await walk(fullPath);
} else if (entry.name.endsWith(".ts")) {
files.push(fullPath);
}
}
}
await walk(dir);
return files;
}
async function getAllPythonFiles(dir: string): Promise<string[]> {
const files: string[] = [];
async function walk(currentDir: string) {
const entries = await readdir(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(currentDir, entry.name);
if (entry.isDirectory()) {
await walk(fullPath);
} else if (entry.name.endsWith(".py")) {
files.push(fullPath);
}
}
}
await walk(dir);
return files;
}
async function getAllAssetFiles(dir: string): Promise<string[]> {
const files: string[] = [];
const assetExtensions = [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico", ".js", ".css"];
async function walk(currentDir: string) {
const entries = await readdir(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(currentDir, entry.name);
if (entry.isDirectory()) {
await walk(fullPath);
} else if (assetExtensions.some((ext) => entry.name.toLowerCase().endsWith(ext))) {
files.push(fullPath);
}
}
}
await walk(dir);
return files;
}
/**
* Replace MkDocs variable references like {{ variable_name }}
*/
function replaceMkdocsVariables(content: string): string {
let result = content;
for (const [varName, value] of Object.entries(MKDOCS_VARIABLES)) {
// Match {{ variable_name }} with optional whitespace
const pattern = new RegExp(`\\{\\{\\s*${varName}\\s*\\}\\}`, "g");
result = result.replace(pattern, value);
}
return result;
}
/**
* Replace ts_not_supported() macro calls
* Generates: !!! info "Not supported in TypeScript"\n {message}
*/
function replaceTsNotSupported(content: string): string {
// Match {{ ts_not_supported() }} or {{ ts_not_supported("message") }}
const pattern = /\{\{\s*ts_not_supported\(\s*(?:"([^"]*)"|'([^']*)')?\s*\)\s*\}\}/g;
return content.replace(pattern, (_match, doubleQuoted, singleQuoted) => {
const message = doubleQuoted ?? singleQuoted ?? DEFAULT_TS_NOT_SUPPORTED;
return `:::note[Not supported in TypeScript]\n${message}\n:::`;
});
}
/**
* Replace ts_not_supported_code() macro calls
* Generates TypeScript code tab with comment
*/
function replaceTsNotSupportedCode(content: string): string {
// Match {{ ts_not_supported_code() }} or {{ ts_not_supported_code("message") }}
const pattern = /\{\{\s*ts_not_supported_code\(\s*(?:"([^"]*)"|'([^']*)')?\s*\)\s*\}\}/g;
return content.replace(pattern, (_match, doubleQuoted, singleQuoted) => {
const message = doubleQuoted ?? singleQuoted ?? DEFAULT_TS_NOT_SUPPORTED_CODE;
return `=== "TypeScript"\n\n \`\`\`ts\n // ${message}\n \`\`\``;
});
}
/**
* Remove experimental_feature_warning() macro from content
* The experimental status is tracked via frontmatter instead
*/
function replaceExperimentalWarning(content: string): string {
// Only match the exact macro with no arguments: {{ experimental_feature_warning() }}
const pattern = /\{\{\s*experimental_feature_warning\(\)\s*\}\}\n?/g;
return content.replace(pattern, "");
}
/**
* Convert MkDocs tabs syntax to <tabs>/<tab> format
* MkDocs format:
* === "Label"
* content (indented 4 spaces)
* === "Label2"
* content (indented 4 spaces)
*
* New format:
* <tabs>
* <tab label="Label">
* content (no indent)
* </tab>
* <tab label="Label2">
* content (no indent)
* </tab>
* </tabs>
*/
function convertMkdocsTabs(content: string): string {
const lines = content.split("\n");
const result: string[] = [];
let i = 0;
while (i < lines.length) {
const line = lines[i];
// Match tab start: === "Label" or === 'Label'
const tabMatch = line.match(/^(\s*)===\s+["']([^"']+)["']\s*$/);
if (tabMatch) {
const [, leadingWhitespace, firstLabel] = tabMatch;
const baseIndent = leadingWhitespace.length;
const contentIndent = baseIndent + 4;
// Start collecting tabs
const tabs: Array<{ label: string; content: string[] }> = [];
// Process first tab
let currentLabel = firstLabel;
let currentContent: string[] = [];
i++;
while (i < lines.length) {
const currentLine = lines[i];
// Check for next tab at same indentation level
const nextTabMatch = currentLine.match(/^(\s*)===\s+["']([^"']+)["']\s*$/);
if (nextTabMatch && nextTabMatch[1].length === baseIndent) {
// Save current tab and start new one
tabs.push({ label: currentLabel, content: currentContent });
currentLabel = nextTabMatch[2];
currentContent = [];
i++;
continue;
}
// Check if we've exited the tab block (non-empty line at base indent or less)
if (currentLine.trim() !== "") {
const lineIndent = currentLine.match(/^(\s*)/)?.[1].length ?? 0;
if (lineIndent < contentIndent && !currentLine.match(/^(\s*)===\s+["']/)) {
// We've exited the tabs block
break;
}
}
// Handle content lines
if (currentLine.trim() === "") {
// Empty line - check if we're still in tabs
let nextNonEmpty = i + 1;
while (nextNonEmpty < lines.length && lines[nextNonEmpty].trim() === "") {
nextNonEmpty++;
}
if (nextNonEmpty < lines.length) {
const nextLine = lines[nextNonEmpty];
const nextTabMatch = nextLine.match(/^(\s*)===\s+["']([^"']+)["']\s*$/);
const nextIndent = nextLine.match(/^(\s*)/)?.[1].length ?? 0;
// Continue if next content is indented or is another tab
if (nextIndent >= contentIndent || (nextTabMatch && nextTabMatch[1].length === baseIndent)) {
currentContent.push("");
i++;
continue;
}
}
// End of tabs block
break;
}
// Content line - remove the 4-space indentation
const lineIndent = currentLine.match(/^(\s*)/)?.[1].length ?? 0;
if (lineIndent >= contentIndent) {
currentContent.push(currentLine.slice(contentIndent));
} else {
currentContent.push(currentLine.slice(lineIndent));
}
i++;
}
// Save last tab
tabs.push({ label: currentLabel, content: currentContent });
// Only convert if we have multiple tabs (single === might be something else)
if (tabs.length >= 1) {
// Build the new tabs format
result.push(`${leadingWhitespace}<Tabs>`);
for (const tab of tabs) {
result.push(`${leadingWhitespace}<Tab label="${tab.label}">`);
// Trim trailing empty lines from content
while (tab.content.length > 0 && tab.content[tab.content.length - 1].trim() === "") {
tab.content.pop();
}
// Add content without extra indentation
for (const contentLine of tab.content) {
result.push(`${leadingWhitespace}${contentLine}`);
}
result.push(`${leadingWhitespace}</Tab>`);
}
result.push(`${leadingWhitespace}</Tabs>`);
} else {
// Not a valid tabs block, restore original
result.push(line);
}
} else {
result.push(line);
i++;
}
}
return result.join("\n");
}
/**
* Map MkDocs admonition types to Astro aside types
* MkDocs types: note, abstract, info, tip, success, question, warning, failure, danger, bug, example, quote
* Astro types: note, tip, caution, danger
*/
function mapAdmonitionType(mkdocsType: string): string {
const typeMap: Record<string, string> = {
note: "note",
abstract: "note",
summary: "note",
tldr: "note",
info: "note",
todo: "note",
tip: "tip",
hint: "tip",
important: "tip",
success: "tip",
check: "tip",
done: "tip",
question: "note",
help: "note",
faq: "note",
warning: "caution",
caution: "caution",
attention: "caution",
failure: "danger",
fail: "danger",
missing: "danger",
danger: "danger",
error: "danger",
bug: "danger",
example: "note",
snippet: "note",
quote: "note",
cite: "note",
};
return typeMap[mkdocsType.toLowerCase()] ?? "note";
}
/**
* Convert MkDocs admonitions to Astro asides
* MkDocs format: !!! type "Title"\n content (indented)
* Astro format: :::type[Title]\ncontent\n:::
*/
function convertAdmonitions(content: string): string {
const lines = content.split("\n");
const result: string[] = [];
let i = 0;
while (i < lines.length) {
const line = lines[i];
// Match admonition start: !!! type "Title" or !!! type 'Title' or !!! type
// Also matches collapsible variants: ??? type and ???+ type
const admonitionMatch = line.match(/^(\s*)(?:!!!|\?\?\?[+]?)\s+(\w+)(?:\s+["']([^"']+)["'])?\s*$/);
if (admonitionMatch) {
const [, leadingWhitespace, type, title] = admonitionMatch;
const astroType = mapAdmonitionType(type);
const titlePart = title ? `[${title}]` : "";
// Collect indented content lines (4 spaces more than the !!! line)
const contentLines: string[] = [];
const baseIndent = leadingWhitespace.length;
const contentIndent = baseIndent + 4;
i++;
while (i < lines.length) {
const contentLine = lines[i];
// Check if line is indented content (at least 4 spaces more than base)
// or is an empty line (which could be part of the admonition)
if (contentLine.trim() === "") {
// Empty line - could be part of admonition or separator
// Look ahead to see if next non-empty line is still indented
let nextNonEmpty = i + 1;
while (nextNonEmpty < lines.length && lines[nextNonEmpty].trim() === "") {
nextNonEmpty++;
}
if (nextNonEmpty < lines.length) {
const nextLine = lines[nextNonEmpty];
const nextIndent = nextLine.match(/^(\s*)/)?.[1].length ?? 0;
if (nextIndent >= contentIndent) {
// Next content is still indented, include empty line
contentLines.push("");
i++;
continue;
}
}
// End of admonition
break;
}
const lineIndent = contentLine.match(/^(\s*)/)?.[1].length ?? 0;
if (lineIndent >= contentIndent) {
// Remove the content indentation (4 spaces relative to base)
contentLines.push(contentLine.slice(contentIndent));
i++;
} else {
// Line is not indented enough, end of admonition
break;
}
}
// Build the Astro aside
result.push(`${leadingWhitespace}:::${astroType}${titlePart}`);
for (const contentLine of contentLines) {
result.push(`${leadingWhitespace}${contentLine}`);
}
result.push(`${leadingWhitespace}:::`);
} else {
result.push(line);
i++;
}
}
return result.join("\n");
}
/**
* Convert HTML comments to JSX comments
* HTML format: <!-- comment -->
* JSX format: \{/* comment *\/\}
*/
function convertHtmlCommentsToJsx(content: string): string {
// Match HTML comments: <!-- ... -->
// Use non-greedy match to handle multiple comments
return content.replace(/<!--([\s\S]*?)-->/g, (_match, commentContent) => {
return `{/*${commentContent}*/}`;
});
}
/**
* Convert HTML <br> tags to self-closing JSX <br /> tags
* MDX requires self-closing tags
*/
function convertBrToSelfClosing(content: string): string {
// Match <br> that isn't already self-closing (not followed by optional whitespace and /)
return content.replace(/<br\s*(?!\/)>/gi, "<br />");
}
/**
* Convert old MkDocs-style API reference links to the new @api shorthand format.
*
* Old formats:
* - Python: `../api-reference/python/agent/agent_result.md#strands.agent.agent_result.AgentResult`
* - TypeScript: `../api-reference/typescript/classes/BedrockModel.html`
*
* New formats:
* - Python: `@api/python/strands.agent.agent_result#AgentResult`
* - TypeScript: `@api/typescript/BedrockModel`
*/
function convertApiLinks(content: string): string {
// Match markdown links with potentially nested brackets in the text
// This handles cases like [`list[ToolSpec]`](url)
const markdownLinkPattern = /\[([^\]]*(?:\[[^\]]*\][^\]]*)*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
return content.replace(markdownLinkPattern, (match, text, url) => {
if (isOldApiLink(url)) {
const newUrl = convertApiLink(url);
if (newUrl) {
return `[${text}](${newUrl})`;
}
}
return match;
});
}
/**
* Remove community_contribution_banner macro from content
* The banner is rendered via a component based on `community: true` frontmatter
*/
function removeCommunityBannerMacro(content: string): string {
const pattern = /\{\{\s*community_contribution_banner\s*\}\}\n?/g;
return content.replace(pattern, "");
}
/**
* Remove the Language Support info block from content
* The language support is indicated via `languages: Python` frontmatter
*/
function removeLanguageSupportBlock(content: string): string {
const lines = content.split("\n");
const result: string[] = [];
let i = 0;
while (i < lines.length) {
const line = lines[i];
// Check for the language support info block
if (line === INFO_BLOCK_PATTERN) {
i++;
// Skip the content line if it matches
if (i < lines.length && lines[i] === INFO_BLOCK_CONTENT) {
i++;
}
// Skip trailing blank line if present
if (i < lines.length && lines[i].trim() === "") {
i++;
}
continue;
}
result.push(line);
i++;
}
return result.join("\n");
}
/**
* Extract the first L1 heading (# Title) from content
* Returns the title text or null if not found
*/
function extractH1Title(content: string): string | null {
// Match # Title at the start of a line (not inside code blocks)
const lines = content.split("\n");
let inCodeBlock = false;
for (const line of lines) {
// Track code blocks to avoid matching headings inside them
if (line.trim().startsWith("```")) {
inCodeBlock = !inCodeBlock;
continue;
}
if (!inCodeBlock) {
const h1Match = line.match(/^#\s+(.+)$/);
if (h1Match) {
return h1Match[1].trim();
}
}
}
return null;
}
/**
* Check if frontmatter already has a title field
*/
function frontmatterHasTitle(content: string): boolean {
const lines = content.split("\n");
if (lines[0] !== "---") return false;
for (let i = 1; i < lines.length; i++) {
if (lines[i] === "---") break;
if (lines[i].match(/^title:\s*.+$/)) {
return true;
}
}
return false;
}
/**
* Remove the first L1 heading from content
*/
function removeH1Heading(content: string): string {
const lines = content.split("\n");
const result: string[] = [];
let inCodeBlock = false;
let removedH1 = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Track code blocks
if (line.trim().startsWith("```")) {
inCodeBlock = !inCodeBlock;
result.push(line);
continue;
}
if (!inCodeBlock && !removedH1) {
const h1Match = line.match(/^#\s+.+$/);
if (h1Match) {
removedH1 = true;
// Skip blank line after H1 if present
if (i + 1 < lines.length && lines[i + 1].trim() === "") {
i++;
}
continue;
}
}
result.push(line);
}
return result.join("\n");
}
function processFile(content: string, explicitTitle?: string, hasCommunityLabel?: boolean, sidebarInfo?: SidebarInfo, isBidiPage?: boolean): { modified: boolean; newContent: string } {
// Detect features BEFORE any transformations
const hasLanguageBlock = content.includes(INFO_BLOCK_PATTERN);
const hasCommunityBanner = content.includes(COMMUNITY_BANNER);
// Only match the exact macro with no arguments
const hasExperimentalWarningMacro = content.includes("{{ experimental_feature_warning() }}");
const alreadyHasLanguages = content.includes("languages:");
const alreadyHasCommunity = content.includes("community:");
const alreadyHasExperimental = content.includes("experimental:");
const alreadyHasSidebar = content.includes("sidebar:");
// Determine what frontmatter needs to be added based on original content
const needsLanguages = hasLanguageBlock && !alreadyHasLanguages;
const needsCommunity = hasCommunityBanner && !alreadyHasCommunity;
// Apply all macro replacements and conversions
let newContent = content;
newContent = replaceMkdocsVariables(newContent);
newContent = replaceTsNotSupported(newContent);
newContent = replaceTsNotSupportedCode(newContent);
newContent = replaceExperimentalWarning(newContent);
newContent = removeCommunityBannerMacro(newContent);
newContent = removeLanguageSupportBlock(newContent);
newContent = convertAdmonitions(newContent);
newContent = convertMkdocsTabs(newContent);
newContent = convertHtmlCommentsToJsx(newContent);
newContent = convertBrToSelfClosing(newContent);
newContent = convertApiLinks(newContent);
// Handle H1 heading and title frontmatter
const h1Title = extractH1Title(newContent);
const hasExistingTitle = frontmatterHasTitle(newContent);
let titleToUse = explicitTitle ?? h1Title;
// Check if title contains [Experimental] and strip it
const hasExperimentalInTitle = titleToUse?.includes("[Experimental]") ?? false;
if (hasExperimentalInTitle && titleToUse) {
titleToUse = titleToUse.replace(/\s*\[Experimental\]\s*/g, "").trim();
}
// Determine if file needs experimental frontmatter (from title or macro)
const hasExperimentalContent = hasExperimentalInTitle || hasExperimentalWarningMacro;
const needsExperimental = hasExperimentalContent && !alreadyHasExperimental;
const needsTitle = titleToUse && !hasExistingTitle;
// Determine sidebar badge type (experimental takes precedence, then nav badge, then community)
// Nav badges from <sup> tags: "new", "community", etc.
const navBadge = sidebarInfo?.badge;
const needsExperimentalBadge = hasExperimentalContent && !alreadyHasSidebar && !isBidiPage;
const needsNavBadge = navBadge && !alreadyHasSidebar && !needsExperimentalBadge;
const needsCommunityBadge = hasCommunityLabel && !alreadyHasSidebar && !needsExperimentalBadge && !needsNavBadge;
// Determine if sidebar label is needed (when label differs from title)
const sidebarLabel = sidebarInfo?.label;
const needsSidebarLabel = sidebarLabel && sidebarLabel !== titleToUse && !alreadyHasSidebar;
// Determine if any sidebar config is needed
const needsSidebar = needsExperimentalBadge || needsNavBadge || needsCommunityBadge || needsSidebarLabel;
// If there's an H1 heading, remove it (title goes in frontmatter)
if (h1Title) {
newContent = removeH1Heading(newContent);
}
// Check if content was transformed
const contentTransformed = newContent !== content;
// Determine if any modifications are needed
const needsModification = contentTransformed || needsLanguages || needsCommunity || needsExperimental || needsTitle || needsSidebar;
if (!needsModification) {
return { modified: false, newContent: content };
}
const lines = newContent.split("\n");
const newLines: string[] = [];
const hasFrontMatter = lines[0] === "---";
let inFrontMatter = false;
let addedLanguages = alreadyHasLanguages;
let addedCommunity = alreadyHasCommunity;
let addedExperimental = alreadyHasExperimental;
let addedTitle = hasExistingTitle;
let addedSidebar = alreadyHasSidebar;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Handle front-matter
if (i === 0 && line === "---") {
inFrontMatter = true;
newLines.push(line);
continue;
}
if (inFrontMatter && line === "---") {
// End of front-matter - add fields before closing ---
if (titleToUse && !addedTitle) {
// Escape quotes in title for YAML
const escapedTitle = titleToUse.includes(":") || titleToUse.includes('"') || titleToUse.includes("'")
? `"${titleToUse.replace(/"/g, '\\"')}"`
: titleToUse;
newLines.push(`title: ${escapedTitle}`);
addedTitle = true;
}
if (hasLanguageBlock && !addedLanguages) {
newLines.push("languages: Python");
addedLanguages = true;
}
if (hasCommunityBanner && !addedCommunity) {
newLines.push("community: true");
addedCommunity = true;
}
if (hasExperimentalContent && !addedExperimental) {
newLines.push("experimental: true");
addedExperimental = true;
}
// Add sidebar config (label and/or badge)
if (needsSidebar && !addedSidebar) {
newLines.push("sidebar:");
if (needsSidebarLabel) {
newLines.push(` label: "${sidebarLabel}"`);
}
if (needsExperimentalBadge) {
newLines.push(" badge:");
newLines.push(" text: Experimental");
newLines.push(" variant: note");
} else if (needsNavBadge) {
// Capitalize first letter of badge text
const badgeText = navBadge.charAt(0).toUpperCase() + navBadge.slice(1);
newLines.push(" badge:");
newLines.push(` text: ${badgeText}`);
newLines.push(" variant: note");
} else if (needsCommunityBadge) {
newLines.push(" badge:");
newLines.push(" text: Community");
newLines.push(" variant: note");
}
addedSidebar = true;
}
inFrontMatter = false;
newLines.push(line);
continue;
}
newLines.push(line);
}
// If no front-matter existed, add it at the beginning
if (!hasFrontMatter) {
const frontMatterFields: string[] = [];
if (titleToUse) {
const escapedTitle = titleToUse.includes(":") || titleToUse.includes('"') || titleToUse.includes("'")
? `"${titleToUse.replace(/"/g, '\\"')}"`
: titleToUse;
frontMatterFields.push(`title: ${escapedTitle}`);
}
if (hasLanguageBlock) frontMatterFields.push("languages: Python");
if (hasCommunityBanner) frontMatterFields.push("community: true");
if (hasExperimentalContent) frontMatterFields.push("experimental: true");
// Add sidebar config (label and/or badge)
if (needsSidebar) {
frontMatterFields.push("sidebar:");
if (needsSidebarLabel) {
frontMatterFields.push(` label: "${sidebarLabel}"`);
}
if (needsExperimentalBadge) {
frontMatterFields.push(" badge:");
frontMatterFields.push(" text: Experimental");
frontMatterFields.push(" variant: note");
} else if (needsNavBadge) {
// Capitalize first letter of badge text
const badgeText = navBadge.charAt(0).toUpperCase() + navBadge.slice(1);
frontMatterFields.push(" badge:");
frontMatterFields.push(` text: ${badgeText}`);
frontMatterFields.push(" variant: note");
} else if (needsCommunityBadge) {
frontMatterFields.push(" badge:");
frontMatterFields.push(" text: Community");
frontMatterFields.push(" variant: note");
}
}
if (frontMatterFields.length > 0) {
newLines.unshift("---", ...frontMatterFields, "---", "");
}
}
const finalContent = newLines.join("\n");
return { modified: finalContent !== content, newContent: finalContent };
}
async function main() {
console.log(`Scanning ${DOCS_DIR} for markdown files...`);
console.log(`Output directory: ${OUTPUT_DIR}`);
// Ensure output directory exists (use docs:revert to clean before re-running)
await mkdir(OUTPUT_DIR, { recursive: true });
// Load community-labeled files from mkdocs.yml nav
const communityLabeledFiles = getCommunityLabeledFiles(MKDOCS_PATH);
console.log(`Found ${communityLabeledFiles.size} community-labeled files in nav`);
// Load sidebar labels from mkdocs.yml nav
const sidebarLabels = getSidebarLabels(MKDOCS_PATH);
console.log(`Found ${sidebarLabels.size} sidebar labels in nav`);
const files = await getAllMarkdownFiles(DOCS_DIR);
let processedCount = 0;
for (const file of files) {
// Skip special-cased files
if (SKIP_FILES.some((skip) => file.endsWith(skip))) {
continue;
}
// Skip files matching skip patterns (e.g., index files in examples)
if (SKIP_PATTERNS.some((pattern) => pattern.test(file))) {
console.log(`⊘ Skipped (pattern): ${file}`);
continue;
}
const content = await readFile(file, "utf-8");
// Check if this file needs an explicit title
const relativePath = file.replace(`${DOCS_DIR}/`, "");
const explicitTitle = EXPLICIT_TITLES[relativePath];
// Check if this file has a community label in the nav
const hasCommunityLabel = communityLabeledFiles.has(relativePath);
// Get sidebar info from nav (label and badge)
const sidebarInfo = sidebarLabels.get(relativePath);
// Check if this is a bidi (bidirectional-streaming) page — skip experimental badge for these
const isBidiPage = relativePath.startsWith("user-guide/concepts/bidirectional-streaming/");
const { newContent } = processFile(content, explicitTitle, hasCommunityLabel, sidebarInfo, isBidiPage);
// Determine output path (convert .md to .mdx and write to OUTPUT_DIR)
const outputRelativePath = relativePath.replace(/\.md$/, ".mdx");
const outputPath = join(OUTPUT_DIR, outputRelativePath);
// Ensure output directory exists
await mkdir(dirname(outputPath), { recursive: true });
// Write processed content to output directory
await writeFile(outputPath, newContent, "utf-8");
// Delete the original source file
await unlink(file);
console.log(`✓ ${relativePath} → ${outputRelativePath}`);
processedCount++;
}
console.log(`\nDone! Processed ${processedCount} markdown file(s) to ${OUTPUT_DIR}.`);
// Copy TypeScript files (used for code snippets)
const tsFiles = await getAllTypeScriptFiles(DOCS_DIR);
let tsCopiedCount = 0;
for (const file of tsFiles) {
const relativePath = file.replace(`${DOCS_DIR}/`, "");
const outputPath = join(OUTPUT_DIR, relativePath);
// Ensure output directory exists
await mkdir(dirname(outputPath), { recursive: true });
// Read and write the file (simple copy)
const content = await readFile(file, "utf-8");
await writeFile(outputPath, content, "utf-8");
// Delete the original source file
await unlink(file);
console.log(`✓ Copied: ${relativePath}`);
tsCopiedCount++;
}
console.log(`Copied ${tsCopiedCount} TypeScript file(s).`);
// Copy Python files (used for code snippets)
const pyFiles = await getAllPythonFiles(DOCS_DIR);
let pyCopiedCount = 0;
for (const file of pyFiles) {
const relativePath = file.replace(`${DOCS_DIR}/`, "");
const outputPath = join(OUTPUT_DIR, relativePath);
// Ensure output directory exists
await mkdir(dirname(outputPath), { recursive: true });
// Read and write the file (simple copy)
const content = await readFile(file, "utf-8");
await writeFile(outputPath, content, "utf-8");
// Delete the original source file
await unlink(file);
console.log(`✓ Copied: ${relativePath}`);
pyCopiedCount++;
}
console.log(`Copied ${pyCopiedCount} Python file(s).`);
// Copy asset files (images, JS, CSS, etc.)
const assetFiles = await getAllAssetFiles(DOCS_DIR);
let assetCopiedCount = 0;
for (const file of assetFiles) {
const relativePath = file.replace(`${DOCS_DIR}/`, "");
const outputPath = join(OUTPUT_DIR, relativePath);
// Ensure output directory exists
await mkdir(dirname(outputPath), { recursive: true });
// Read and write the file as binary
const content = await readFile(file);
await writeFile(outputPath, content);
// Delete the original source file
await unlink(file);
console.log(`✓ Copied asset: ${relativePath}`);
assetCopiedCount++;
}
console.log(`Copied ${assetCopiedCount} asset file(s).`);
// Run special-case page updates
await updateQuickstart();
await updateLanguageIndexFiles();
}
main().catch(console.error);
+2 -1
View File
@@ -16,7 +16,8 @@ import Search from 'virtual:starlight/components/Search';
import ThemeSelect from 'virtual:starlight/components/ThemeSelect';
import { Icon } from '@astrojs/starlight/components';
import { navLinks, githubSections, type NavLink, type GitHubSection } from '../../config/navbar';
import { navLinks, githubSections, type NavLink } from '../../config/navbar';
import type { GitHubSection } from '../../sidebar';
import GitHubDropdown from '../GitHubDropdown.astro';
import { findCurrentNavSection } from '../../route-middleware';
import logoHeaderDark from '../../assets/logo-header-dark.svg';
+13 -77
View File
@@ -1,10 +1,13 @@
/**
* Navigation bar links configuration.
* Navigation bar links and GitHub dropdown configuration.
*
* Add, remove, or reorder links here to customize the header navigation tabs.
* These appear as tabs below the main header on desktop viewports.
* This module reads navigation configuration from navigation.yml and exports
* processed data for use in Header component.
*/
import path from 'node:path'
import { loadNavigationConfig, type GitHubSection } from '../sidebar'
export interface NavLink {
/** Display label for the link */
label: string
@@ -55,51 +58,11 @@ function transformNavLinks(links: NavLink[]): NavLink[] {
})
}
/**
* Raw navigation links (without base path).
* Order here determines display order.
*/
const rawNavLinks: NavLink[] = [
{
label: 'Home',
href: '/',
},
{
label: 'User Guide',
href: '/docs/user-guide/quickstart/overview/',
basePath: '/docs/user-guide/',
},
{
label: 'Examples',
href: '/docs/examples/',
basePath: '/docs/examples/',
},
{
label: 'Community',
href: '/docs/community/community-packages/',
basePath: '/docs/community/',
},
{
label: 'Labs',
href: '/docs/labs/',
basePath: '/docs/labs/',
},
{
label: 'Contribute ❤️',
href: '/docs/contribute/',
basePath: '/docs/contribute/',
},
{
label: 'Python API',
href: '/docs/api/python/',
basePath: '/docs/api/python/',
},
{
label: 'TypeScript API',
href: '/docs/api/typescript/',
basePath: '/docs/api/typescript/',
},
]
// Load configuration from navigation.yml
const configPath = path.resolve('./src/config/navigation.yml')
const config = loadNavigationConfig(configPath)
const rawNavLinks: NavLink[] = config.navbar || []
const rawGithubSections: GitHubSection[] = config.github?.sections || []
/**
* Navigation links with base path applied.
@@ -108,33 +71,6 @@ const rawNavLinks: NavLink[] = [
export const navLinks: NavLink[] = transformNavLinks(rawNavLinks)
/**
* GitHub links shown in the header dropdown (desktop) and mobile nav menu.
* Grouped into sections with a title and list of links.
* GitHub sections for the header dropdown.
*/
export interface GitHubLink {
label: string
href: string
icon?: string
}
export interface GitHubSection {
title: string
links: GitHubLink[]
}
export const githubSections: GitHubSection[] = [
{
title: 'SDKs',
links: [
{ label: 'sdk-python', href: 'https://github.com/strands-agents/sdk-python', icon: 'PY' },
{ label: 'sdk-typescript', href: 'https://github.com/strands-agents/sdk-typescript', icon: 'TS' },
],
},
{
title: 'Organizations',
links: [
{ label: 'strands-agents', href: 'https://github.com/strands-agents' },
{ label: 'strands-labs', href: 'https://github.com/strands-labs' },
],
},
]
export const githubSections: GitHubSection[] = rawGithubSections
+260
View File
@@ -0,0 +1,260 @@
# Navigation configuration for Strands Agents SDK documentation
# This file combines navbar, sidebar, and GitHub dropdown configuration.
#
# STRUCTURE:
# - navbar: Top navigation bar links
# - sidebar: Hierarchical sidebar structure (slugs only for files - labels from frontmatter)
# - github: GitHub dropdown sections
#
# NOTE: Badges (like "new", "community") should come from page frontmatter only,
# not from this configuration file.
navbar:
- label: Home
href: /
- label: User Guide
href: /docs/user-guide/quickstart/overview/
basePath: /docs/user-guide/
- label: Examples
href: /docs/examples/
basePath: /docs/examples/
- label: Community
href: /docs/community/community-packages/
basePath: /docs/community/
- label: Labs
href: /docs/labs/
basePath: /docs/labs/
- label: Contribute ❤️
href: /docs/contribute/
basePath: /docs/contribute/
- label: Python API
href: /docs/api/python/
basePath: /docs/api/python/
- label: TypeScript API
href: /docs/api/typescript/
basePath: /docs/api/typescript/
sidebar:
- label: User Guide
items:
- docs/index
- label: Quickstart
items:
- docs/user-guide/quickstart/overview
- docs/user-guide/quickstart/python
- docs/user-guide/quickstart/typescript
- 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
- 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: 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: Streaming
items:
- docs/user-guide/concepts/streaming
- docs/user-guide/concepts/streaming/async-iterators
- docs/user-guide/concepts/streaming/callback-handlers
- label: Multi-agent
items:
- docs/user-guide/concepts/multi-agent/agent-to-agent
- docs/user-guide/concepts/multi-agent/agents-as-tools
- 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:
- docs/user-guide/concepts/bidirectional-streaming/models/nova_sonic
- docs/user-guide/concepts/bidirectional-streaming/models/gemini_live
- docs/user-guide/concepts/bidirectional-streaming/models/openai_realtime
- docs/user-guide/concepts/bidirectional-streaming/io
- docs/user-guide/concepts/bidirectional-streaming/events
- docs/user-guide/concepts/bidirectional-streaming/interruption
- docs/user-guide/concepts/bidirectional-streaming/hooks
- docs/user-guide/concepts/bidirectional-streaming/session-management
- label: Experimental
items:
- docs/user-guide/concepts/experimental/agent-config
- docs/user-guide/concepts/experimental/steering
- label: Safety & Security
items:
- docs/user-guide/safety-security/responsible-ai
- docs/user-guide/safety-security/guardrails
- docs/user-guide/safety-security/prompt-engineering
- docs/user-guide/safety-security/pii-redaction
- label: Observability & Debugging
items:
- docs/user-guide/observability-evaluation/observability
- docs/user-guide/observability-evaluation/metrics
- docs/user-guide/observability-evaluation/traces
- docs/user-guide/observability-evaluation/logs
- label: Strands Evals SDK
items:
- docs/user-guide/evals-sdk/quickstart
- docs/user-guide/evals-sdk/eval-sop
- label: Evaluators
items:
- docs/user-guide/evals-sdk/evaluators
- docs/user-guide/evals-sdk/evaluators/output_evaluator
- docs/user-guide/evals-sdk/evaluators/trajectory_evaluator
- docs/user-guide/evals-sdk/evaluators/interactions_evaluator
- docs/user-guide/evals-sdk/evaluators/helpfulness_evaluator
- docs/user-guide/evals-sdk/evaluators/faithfulness_evaluator
- docs/user-guide/evals-sdk/evaluators/goal_success_rate_evaluator
- docs/user-guide/evals-sdk/evaluators/tool_selection_evaluator
- docs/user-guide/evals-sdk/evaluators/tool_parameter_evaluator
- docs/user-guide/evals-sdk/evaluators/custom_evaluator
- docs/user-guide/evals-sdk/experiment_generator
- label: Simulators
items:
- docs/user-guide/evals-sdk/simulators
- docs/user-guide/evals-sdk/simulators/user_simulation
- label: How-To Guides
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
items:
- docs/examples
- docs/examples/python/cli-reference-agent
- docs/examples/python/weather_forecaster
- docs/examples/python/memory_agent
- docs/examples/python/file_operations
- docs/examples/python/agents_workflows
- docs/examples/python/knowledge_base_agent
- docs/examples/python/structured_output
- docs/examples/python/multi_agent_example/multi_agent_example
- docs/examples/python/graph_loops_example
- docs/examples/python/meta_tooling
- docs/examples/python/mcp_calculator
- docs/examples/python/multimodal
- label: Community
items:
- docs/community/community-packages
- docs/community/get-featured
- label: Integrations
items:
- docs/community/integrations/ag-ui
- label: Model Providers
items:
- docs/community/model-providers/cohere
- docs/community/model-providers/clova-studio
- docs/community/model-providers/fireworksai
- docs/community/model-providers/nebius-token-factory
- docs/community/model-providers/nvidia-nim
- docs/community/model-providers/sglang
- docs/community/model-providers/vllm
- docs/community/model-providers/mlx
- docs/community/model-providers/xai
- label: Session Managers
items:
- docs/community/session-managers/agentcore-memory
- docs/community/session-managers/strands-valkey-session-manager
- label: Tool Protocols
items:
- docs/community/tools/utcp
- label: Tools
items:
- docs/community/tools/strands-deepgram
- docs/community/tools/strands-hubspot
- docs/community/tools/strands-teams
- docs/community/tools/strands-telegram
- docs/community/tools/strands-telegram-listener
- label: Labs
items:
- docs/labs
- label: Projects
items:
- docs/labs/robots
- docs/labs/robots-sim
- docs/labs/ai-functions
- label: Contribute ❤️
items:
- 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:
- title: SDKs
links:
- label: sdk-python
href: https://github.com/strands-agents/sdk-python
icon: PY
- label: sdk-typescript
href: https://github.com/strands-agents/sdk-typescript
icon: TS
- title: Organizations
links:
- label: strands-agents
href: https://github.com/strands-agents
- label: strands-labs
href: https://github.com/strands-labs
+3 -3
View File
@@ -2,7 +2,7 @@ import type { APIRoute } from 'astro'
import type { CollectionEntry } from 'astro:content'
import { getCollection, getEntry } from 'astro:content'
import { getBase, getSiteOrigin } from '@util/links'
import { loadSidebarFromMkdocs, type StarlightSidebarItem } from '../sidebar'
import { loadSidebarFromConfig, type StarlightSidebarItem } from '../sidebar'
import { renderEntryToMarkdown } from '@util/render-to-markdown'
import path from 'node:path'
@@ -97,8 +97,8 @@ function buildLlmsTxt(docs: CollectionEntry<'docs'>[], sidebar: StarlightSidebar
export const GET: APIRoute = async () => {
const docs = await getCollection('docs')
const sidebar = loadSidebarFromMkdocs(
path.resolve('./mkdocs.yml'),
const sidebar = loadSidebarFromConfig(
path.resolve('./src/config/navigation.yml'),
path.resolve('./src/content')
)
+61 -172
View File
@@ -8,24 +8,41 @@ export type StarlightSidebarItem =
| { label: string; link: string; attrs?: Record<string, string> } // External link
| { label: string; items: StarlightSidebarItem[]; collapsed?: boolean } // Group
interface ConvertContext {
contentDir: string
// Navigation config types
interface NavConfigItem {
label?: string
items?: NavConfigItem[]
}
type NavConfigEntry = string | NavConfigItem
interface NavbarLink {
label: string
href: string
basePath?: string
external?: boolean
}
/**
* Convert mkdocs md path to Starlight slug
*/
export function mdPathToSlug(mdPath: string): string {
let slug = mdPath.replace(/\.md$/, '')
export interface GitHubLink {
label: string
href: string
icon?: string
}
// README.md -> index (or parent directory)
if (slug === 'README') return 'docs/index'
if (slug.endsWith('/README')) return 'docs/' + slug.replace(/\/README$/, '')
export interface GitHubSection {
title: string
links: GitHubLink[]
}
// index.md -> parent directory
if (slug.endsWith('/index')) return 'docs/' + slug.replace(/\/index$/, '')
interface NavigationConfig {
navbar: NavbarLink[]
sidebar: NavConfigItem[]
github: {
sections: GitHubSection[]
}
}
return 'docs/' + slug
interface ConvertContext {
contentDir: string
}
/**
@@ -45,85 +62,25 @@ function contentExists(slug: string, contentDir: string): boolean {
}
/**
* Check if a directory has an index file
* Convert a navigation config item to Starlight sidebar format
*/
function hasIndexFile(dirSlug: string, contentDir: string): boolean {
if (!contentDir) return false
const candidates = [path.join(contentDir, dirSlug, 'index.md'), path.join(contentDir, dirSlug, 'index.mdx')]
return candidates.some((p) => fs.existsSync(p))
}
/**
* Infer the common directory from a group's first item
*/
function inferGroupDir(items: StarlightSidebarItem[]): string | null {
for (const item of items) {
if ('slug' in item && item.slug) {
const parts = item.slug.split('/')
if (parts.length > 1) {
return parts.slice(0, -1).join('/')
}
}
}
return null
}
/**
* Strip HTML tags from label (e.g., <sup> community</sup>)
*/
function cleanLabel(label: string): string {
return label.replace(/<[^>]+>/g, '').trim()
}
/**
* Convert a single mkdocs nav item to Starlight format
*/
export function convertNavItem(item: unknown, ctx: ConvertContext = { contentDir: '' }): StarlightSidebarItem | null {
// String: bare path like "path.md"
function convertConfigItem(item: NavConfigEntry, ctx: ConvertContext): StarlightSidebarItem | null {
// String: slug only (e.g., "docs/user-guide/quickstart/overview")
if (typeof item === 'string') {
const slug = mdPathToSlug(item)
if (!contentExists(slug, ctx.contentDir)) return null
return { slug }
if (!contentExists(item, ctx.contentDir)) return null
return { slug: item }
}
// Object: { "Label": value }
// Object with label and items (group)
if (typeof item === 'object' && item !== null) {
const entries = Object.entries(item)
const first = entries[0]
if (!first) return null
const [rawLabel, value] = first
const label = cleanLabel(rawLabel)
// External link
if (typeof value === 'string' && value.startsWith('http')) {
return { label, link: value, attrs: { target: '_blank' } }
}
// Empty array (placeholder)
if (Array.isArray(value) && value.length === 0) {
return null
}
// Single file: { "Label": "path.md" }
if (typeof value === 'string') {
const slug = mdPathToSlug(value)
if (!contentExists(slug, ctx.contentDir)) return null
return { slug }
}
// Nested group: { "Label": [...] }
if (Array.isArray(value)) {
const children = value
.map((child) => convertNavItem(child, ctx))
if (item.label && item.items) {
const children = item.items
.map((child) => convertConfigItem(child, ctx))
.filter((c): c is StarlightSidebarItem => c !== null)
if (children.length === 0) return null
return { label, items: children }
return { label: item.label, items: children }
}
}
@@ -148,111 +105,43 @@ function applyCollapse(items: StarlightSidebarItem[], depth: number = 0): Starli
}
/**
* Load sidebar from mkdocs.yml
* Load navigation configuration from YAML file
*/
export function loadSidebarFromMkdocs(mkdocsPath: string, docsContentDir?: string): StarlightSidebarItem[] {
let content = fs.readFileSync(mkdocsPath, 'utf-8')
export function loadNavigationConfig(configPath: string): NavigationConfig {
const content = fs.readFileSync(configPath, 'utf-8')
return yaml.load(content) as NavigationConfig
}
// Strip Python-specific YAML tags
content = content.replace(/!!python\/[^\s]+/g, '')
const mkdocs = yaml.load(content) as { nav?: unknown[] }
if (!mkdocs.nav) return []
/**
* Load sidebar from navigation.yml config
*/
export function loadSidebarFromConfig(configPath: string, docsContentDir?: string): StarlightSidebarItem[] {
const config = loadNavigationConfig(configPath)
if (!config.sidebar) return []
const ctx: ConvertContext = { contentDir: docsContentDir || '' }
const items = mkdocs.nav
.map((item) => convertNavItem(item, ctx))
const items = config.sidebar
.map((item) => convertConfigItem(item, ctx))
.filter((i): i is StarlightSidebarItem => i !== null)
.filter((item) => !('link' in item && item.link?.endsWith('.html')))
return applyCollapse(items)
}
/**
* Extract files with <sup> community</sup> in their nav label
* Load navbar links from navigation.yml config
*/
export function getCommunityLabeledFiles(mkdocsPath: string): Set<string> {
const communityFiles = new Set<string>()
let content = fs.readFileSync(mkdocsPath, 'utf-8')
content = content.replace(/!!python\/[^\s]+/g, '')
const mkdocs = yaml.load(content) as { nav?: unknown[] }
if (!mkdocs.nav) return communityFiles
function search(items: unknown[]) {
for (const item of items) {
if (typeof item === 'object' && item !== null) {
for (const [label, value] of Object.entries(item)) {
if (/<sup>\s*community\s*<\/sup>/i.test(label)) {
if (typeof value === 'string' && value.endsWith('.md')) {
communityFiles.add(value)
}
}
if (Array.isArray(value)) {
search(value)
}
}
}
}
}
search(mkdocs.nav)
return communityFiles
}
export interface SidebarInfo {
label: string
badge?: string
export function loadNavbarFromConfig(configPath: string): NavbarLink[] {
const config = loadNavigationConfig(configPath)
return config.navbar || []
}
/**
* Extract sidebar labels and badges for files in the nav
* Returns a map of file path -> { label, badge? }
* Badges are extracted from <sup> tags (e.g., <sup> new</sup> -> "new")
* Load GitHub sections from navigation.yml config
*/
export function getSidebarLabels(mkdocsPath: string): Map<string, SidebarInfo> {
const sidebarLabels = new Map<string, SidebarInfo>()
let content = fs.readFileSync(mkdocsPath, 'utf-8')
content = content.replace(/!!python\/[^\s]+/g, '')
const mkdocs = yaml.load(content) as { nav?: unknown[] }
if (!mkdocs.nav) return sidebarLabels
function search(items: unknown[]) {
for (const item of items) {
if (typeof item === 'object' && item !== null) {
for (const [label, value] of Object.entries(item)) {
// Only capture labels for files (not groups/directories)
if (typeof value === 'string' && value.endsWith('.md')) {
// Extract badge from <sup> tag if present
const supMatch = label.match(/<sup>\s*(\w+)\s*<\/sup>/i)
const badge = supMatch?.[1]?.toLowerCase()
// Clean the label by removing <sup>...</sup> tags entirely (including content)
// then remove any other HTML tags
const cleanedLabel = label
.replace(/<sup>[^<]*<\/sup>/gi, '') // Remove <sup> tags and their content
.replace(/<[^>]+>/g, '') // Remove any other HTML tags
.trim()
const info: SidebarInfo = { label: cleanedLabel }
if (badge) {
info.badge = badge
}
sidebarLabels.set(value, info)
}
if (Array.isArray(value)) {
search(value)
}
}
}
}
}
search(mkdocs.nav)
return sidebarLabels
export function loadGitHubSectionsFromConfig(configPath: string): GitHubSection[] {
const config = loadNavigationConfig(configPath)
return config.github?.sections || []
}
+3 -3
View File
@@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest'
import { getCollection } from 'astro:content'
import path from 'node:path'
import { loadSidebarFromMkdocs, type StarlightSidebarItem } from '../src/sidebar'
import { loadSidebarFromConfig, type StarlightSidebarItem } from '../src/sidebar'
describe('Content Collections', () => {
it('should list all doc contents', async () => {
@@ -22,9 +22,9 @@ describe('Content Collections', () => {
})
it('should have valid slugs for all sidebar items', async () => {
const mkdocsPath = path.resolve('./mkdocs.yml')
const configPath = path.resolve('./src/config/navigation.yml')
const docsDir = path.resolve('./docs')
const sidebar = loadSidebarFromMkdocs(mkdocsPath, docsDir)
const sidebar = loadSidebarFromConfig(configPath, docsDir)
const docs = await getCollection('docs')
// Build a set of valid doc IDs
+7 -7
View File
@@ -6,7 +6,7 @@ import {
expandFirstLevelGroups,
} from '../src/route-middleware'
import { type NavLink } from '../src/config/navbar'
import { loadSidebarFromMkdocs, type StarlightSidebarItem } from '../src/sidebar'
import { loadSidebarFromConfig, type StarlightSidebarItem } from '../src/sidebar'
// Sidebar entry types matching Starlight's runtime structure
type SidebarLink = { type: 'link'; label: string; href: string; isCurrent: boolean }
@@ -119,13 +119,13 @@ describe('findCurrentNavSection', () => {
})
})
describe('Sidebar filtering with live mkdocs.yml data', () => {
const mkdocsPath = path.resolve('./mkdocs.yml')
describe('Sidebar filtering with live navigation.yml data', () => {
const configPath = path.resolve('./src/config/navigation.yml')
const docsDir = path.resolve('./src/content')
const buildTimeSidebar = loadSidebarFromMkdocs(mkdocsPath, docsDir)
const buildTimeSidebar = loadSidebarFromConfig(configPath, docsDir)
const runtimeSidebar = convertToRuntimeFormat(buildTimeSidebar)
it('should have loaded sidebar from mkdocs.yml', () => {
it('should have loaded sidebar from navigation.yml', () => {
expect(runtimeSidebar.length).toBeGreaterThan(0)
console.log(`\nLoaded ${runtimeSidebar.length} top-level sidebar items`)
})
@@ -184,9 +184,9 @@ describe('Sidebar filtering with live mkdocs.yml data', () => {
})
describe('Integration: Full filtering flow', () => {
const mkdocsPath = path.resolve('./mkdocs.yml')
const configPath = path.resolve('./src/config/navigation.yml')
const docsDir = path.resolve('./src/content')
const buildTimeSidebar = loadSidebarFromMkdocs(mkdocsPath, docsDir)
const buildTimeSidebar = loadSidebarFromConfig(configPath, docsDir)
const runtimeSidebar = convertToRuntimeFormat(buildTimeSidebar)
it('should correctly filter sidebar for /examples/ page', () => {
+102 -48
View File
@@ -1,12 +1,18 @@
import { describe, it, expect } from 'vitest'
import path from 'node:path'
import { loadSidebarFromMkdocs, convertNavItem, mdPathToSlug, type StarlightSidebarItem } from '../src/sidebar'
import {
loadSidebarFromConfig,
loadNavigationConfig,
loadNavbarFromConfig,
loadGitHubSectionsFromConfig,
type StarlightSidebarItem,
} from '../src/sidebar'
const pathToMkdocsYaml = path.resolve('./mkdocs.yml')
const pathToNavigationYml = path.resolve('./src/config/navigation.yml')
describe('Sidebar Generation', () => {
it('should generate sidebar structure from mkdocs.yml', () => {
const sidebar = loadSidebarFromMkdocs(pathToMkdocsYaml)
describe('Sidebar Generation from navigation.yml', () => {
it('should generate sidebar structure from navigation.yml', () => {
const sidebar = loadSidebarFromConfig(pathToNavigationYml)
console.log('\n=== Generated Sidebar Structure ===\n')
console.log(JSON.stringify(sidebar, null, 2))
@@ -16,40 +22,86 @@ describe('Sidebar Generation', () => {
expect(sidebar.length).toBeGreaterThan(0)
})
it('should convert README.md to index slug', () => {
expect(mdPathToSlug('README.md')).toBe('docs/index')
expect(mdPathToSlug('examples/README.md')).toBe('docs/examples')
it('should load navigation config with all sections', () => {
const config = loadNavigationConfig(pathToNavigationYml)
expect(config).toBeDefined()
expect(config.navbar).toBeDefined()
expect(Array.isArray(config.navbar)).toBe(true)
expect(config.sidebar).toBeDefined()
expect(Array.isArray(config.sidebar)).toBe(true)
expect(config.github).toBeDefined()
expect(config.github.sections).toBeDefined()
})
it('should strip .md extension and handle index files', () => {
expect(mdPathToSlug('user-guide/quickstart/overview.md')).toBe('docs/user-guide/quickstart/overview')
expect(mdPathToSlug('user-guide/concepts/tools/index.md')).toBe('docs/user-guide/concepts/tools')
it('should load navbar links', () => {
const navbar = loadNavbarFromConfig(pathToNavigationYml)
expect(navbar).toBeDefined()
expect(Array.isArray(navbar)).toBe(true)
expect(navbar.length).toBeGreaterThan(0)
// Check that navbar links have required properties
const firstLink = navbar[0]
expect(firstLink).toHaveProperty('label')
expect(firstLink).toHaveProperty('href')
})
it('should handle external links', () => {
const item = convertNavItem({ 'Contribute ❤️': 'https://github.com/example' })
expect(item).toEqual({
label: 'Contribute ❤️',
link: 'https://github.com/example',
attrs: { target: '_blank' },
})
it('should load GitHub sections', () => {
const sections = loadGitHubSectionsFromConfig(pathToNavigationYml)
expect(sections).toBeDefined()
expect(Array.isArray(sections)).toBe(true)
expect(sections.length).toBeGreaterThan(0)
// Check that sections have required structure
const firstSection = sections[0]
expect(firstSection).toHaveProperty('title')
expect(firstSection).toHaveProperty('links')
expect(Array.isArray(firstSection.links)).toBe(true)
})
it('should handle nested items', () => {
const item = convertNavItem({
Quickstart: [{ 'Getting Started': 'overview.md' }, { Python: 'python.md' }],
})
// Internal links omit labels - Starlight uses page title from frontmatter
expect(item).toEqual({
label: 'Quickstart',
items: [{ slug: 'docs/overview' }, { slug: 'docs/python' }],
})
it('should have correct top-level sidebar sections', () => {
const sidebar = loadSidebarFromConfig(pathToNavigationYml)
// Check that we have the expected top-level sections
const topLevelLabels = sidebar
.filter((item): item is StarlightSidebarItem & { label: string } => 'label' in item)
.map((item) => item.label)
expect(topLevelLabels).toContain('User Guide')
expect(topLevelLabels).toContain('Examples')
expect(topLevelLabels).toContain('Community')
expect(topLevelLabels).toContain('Labs')
expect(topLevelLabels).toContain('Contribute ❤️')
})
it('should omit labels for internal links in final output (uses page title from frontmatter)', () => {
// The final Starlight output should not include labels for internal links
// Starlight will automatically use the page's title frontmatter
const sidebar = loadSidebarFromMkdocs(pathToMkdocsYaml)
it('should have collapsed groups at depth >= 1', () => {
const sidebar = loadSidebarFromConfig(pathToNavigationYml)
// Find the User Guide section
const userGuide = sidebar.find(
(item): item is StarlightSidebarItem & { label: string; items: StarlightSidebarItem[] } =>
'label' in item && item.label === 'User Guide'
)
expect(userGuide).toBeDefined()
if (userGuide) {
// Top level should not be collapsed
expect(userGuide).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'
)
// Nested groups should be collapsed
expect(quickstart).toHaveProperty('collapsed', true)
}
})
it('should have slugs without labels for file 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 {
@@ -70,26 +122,28 @@ describe('Sidebar Generation', () => {
expect(leafItem).toHaveProperty('slug')
expect(leafItem).not.toHaveProperty('label')
})
})
describe('getCommunityLabeledFiles', () => {
it('should extract files with <sup> community</sup> in nav label', async () => {
const { getCommunityLabeledFiles } = await import('../src/sidebar')
const communityFiles = getCommunityLabeledFiles(path.resolve('mkdocs.yml'))
it('should handle external links in sidebar', () => {
const sidebar = loadSidebarFromConfig(pathToNavigationYml)
console.log('\n=== Community Labeled Files ===\n')
console.log([...communityFiles])
// 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 ❤️'
)
expect(communityFiles).toBeDefined()
expect(communityFiles instanceof Set).toBe(true)
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')
)
// Should find the community-labeled model providers
expect(communityFiles.has('user-guide/concepts/model-providers/cohere.md')).toBe(true)
expect(communityFiles.has('user-guide/concepts/model-providers/clova-studio.md')).toBe(true)
expect(communityFiles.has('user-guide/concepts/model-providers/fireworksai.md')).toBe(true)
expect(communityFiles.has('user-guide/concepts/model-providers/nebius-token-factory.md')).toBe(true)
// Should NOT include non-community files
expect(communityFiles.has('user-guide/concepts/model-providers/openai.md')).toBe(false)
// If there's an external link, it should have the right attributes
if (externalLink) {
expect(externalLink).toHaveProperty('attrs')
expect(externalLink.attrs).toHaveProperty('target', '_blank')
}
}
})
})
+4 -5
View File
@@ -2,13 +2,12 @@ import { describe, it, expect } from 'vitest'
import { isOldApiLink, convertApiLink } from '../src/util/api-link-converter'
/**
* Test the convertApiLinks function behavior by testing the underlying utilities
* that update-docs.ts uses.
* Test the API link conversion utilities.
*/
describe('update-docs API link conversion', () => {
describe('API link conversion', () => {
/**
* Simulate the convertApiLinks function from update-docs.ts
* Uses a more robust regex that handles nested brackets in link text
* Helper function to convert API links in markdown content
* Uses a regex that handles nested brackets in link text
*/
function convertApiLinks(content: string): string {
// Match markdown links with potentially nested brackets in the text