mirror of
https://github.com/strands-agents/harness-sdk.git
synced 2026-10-02 02:44:48 +08:00
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:
co-authored by
Strands Agent
Mackenzie Zastrow
parent
79c1ac9a7d
commit
5e3f97c684
+1
-1
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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",
|
||||
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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);
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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 || []
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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')
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user