diff --git a/scripts/build-github-wiki.py b/scripts/build-github-wiki.py
index 4387d5d2c..ce7627dff 100755
--- a/scripts/build-github-wiki.py
+++ b/scripts/build-github-wiki.py
@@ -4,12 +4,14 @@
from __future__ import annotations
import argparse
+import json
import re
import shutil
from pathlib import Path, PurePosixPath
from urllib.parse import urlsplit, urlunsplit
LANGUAGES = {"zh": "简体中文", "en": "English", "ja": "日本語"}
+NAVIGATION_LANGUAGE_CODES = {"cn": "zh", "en": "en", "jp": "ja"}
DOC_EXTENSIONS = (".md", ".mdx", ".html")
CALLOUTS = {"Info": "NOTE", "Note": "NOTE", "Tip": "TIP", "Warning": "WARNING"}
MOVED_SECTIONS = {"platforms", "models", "pipelines", "knowledge", "mcp"}
@@ -134,6 +136,86 @@ def discover_documents(root: Path) -> list[Path]:
return sorted(path for language in LANGUAGES for path in (root / language).rglob("*.mdx"))
+def render_page_items(
+ items: list[object], titles: dict[str, str], *, indent: int = 0
+) -> tuple[list[str], set[str]]:
+ lines: list[str] = []
+ included: set[str] = set()
+ prefix = " " * indent
+ for item in items:
+ if isinstance(item, str):
+ source_name = item.removesuffix(".mdx").removesuffix(".md")
+ title = titles.get(source_name)
+ if title:
+ lines.append(f"{prefix}- [{title}]({page_name(PurePosixPath(source_name))})")
+ included.add(source_name)
+ elif isinstance(item, dict) and not item.get("hidden"):
+ group = item.get("group")
+ children = item.get("pages", [])
+ if group and isinstance(children, list):
+ child_lines, child_pages = render_page_items(children, titles, indent=indent + 2)
+ if child_lines:
+ lines.append(f"{prefix}- **{group}**")
+ lines.extend(child_lines)
+ included.update(child_pages)
+ return lines, included
+
+
+def build_sidebar(root: Path, titles: dict[str, str]) -> str:
+ navigation_path = root / "docs.json"
+ if not navigation_path.is_file():
+ lines = ["## LangBot Documentation", ""]
+ for language, label in LANGUAGES.items():
+ lines.extend((f"### {label}", ""))
+ lines.extend(
+ f"- [{title}]({page_name(PurePosixPath(source))})"
+ for source, title in titles.items()
+ if source.startswith(f"{language}/")
+ )
+ lines.append("")
+ return "\n".join(lines).rstrip() + "\n"
+
+ navigation = json.loads(navigation_path.read_text(encoding="utf-8"))["navigation"]["languages"]
+ nav_by_language = {
+ NAVIGATION_LANGUAGE_CODES[item["language"]]: item
+ for item in navigation
+ if item.get("language") in NAVIGATION_LANGUAGE_CODES
+ }
+ lines = ["## LangBot Documentation", "", "[Home](Home)", ""]
+ for language, label in LANGUAGES.items():
+ language_nav = nav_by_language.get(language, {})
+ open_attribute = " open" if language == "zh" else ""
+ lines.extend((f"", f"{label}
", ""))
+ included: set[str] = set()
+ for tab_index, tab in enumerate(language_nav.get("tabs", [])):
+ tab_lines: list[str] = []
+ tab_pages: set[str] = set()
+ for group in tab.get("groups", []):
+ group_lines, group_pages = render_page_items(group.get("pages", []), titles, indent=2)
+ if group_lines:
+ tab_lines.append(f"- **{group['group']}**")
+ tab_lines.extend(group_lines)
+ tab_pages.update(group_pages)
+ if tab_lines:
+ tab_open = " open" if language == "zh" and tab_index == 0 else ""
+ lines.extend((f"", f"{tab['tab']}
", ""))
+ lines.extend(tab_lines)
+ lines.extend(("", " ", ""))
+ included.update(tab_pages)
+
+ unlisted = sorted(
+ (source, title)
+ for source, title in titles.items()
+ if source.startswith(f"{language}/") and source not in included
+ )
+ if unlisted:
+ lines.extend(("", "Other pages
", ""))
+ lines.extend(f"- [{title}]({page_name(PurePosixPath(source))})" for source, title in unlisted)
+ lines.extend(("", " ", ""))
+ lines.extend((" ", ""))
+ return "\n".join(lines).rstrip() + "\n"
+
+
def build(root: Path, output: Path) -> int:
documents = discover_documents(root)
if not documents:
@@ -142,27 +224,27 @@ def build(root: Path, output: Path) -> int:
shutil.rmtree(output)
output.mkdir(parents=True)
index: dict[str, list[tuple[str, str]]] = {language: [] for language in LANGUAGES}
+ titles: dict[str, str] = {}
for path in documents:
source = PurePosixPath(path.relative_to(root).as_posix())
title, converted = convert_mdx(source, path.read_text(encoding="utf-8"))
name = page_name(source)
(output / f"{name}.md").write_text(converted, encoding="utf-8")
index[source.parts[0]].append((name, title))
+ titles[source.with_suffix("").as_posix()] = title
home = [
"# LangBot Documentation",
"",
"This GitHub Wiki is automatically synchronized from [langbot-app/langbot-wiki](https://github.com/langbot-app/langbot-wiki).",
"",
]
- sidebar = ["## LangBot Documentation", ""]
for language, label in LANGUAGES.items():
pages = index[language]
- home.extend((f"## {label}", "", f"[{pages[0][1]}]({pages[0][0]})", ""))
- sidebar.extend((f"### {label}", ""))
- sidebar.extend(f"- [{title}]({name})" for name, title in pages)
- sidebar.append("")
+ guide_name = f"{language}-insight-guide"
+ guide = next(((name, title) for name, title in pages if name == guide_name), pages[0])
+ home.extend((f"## {label}", "", f"[{guide[1]}]({guide[0]})", ""))
(output / "Home.md").write_text("\n".join(home).rstrip() + "\n", encoding="utf-8")
- (output / "_Sidebar.md").write_text("\n".join(sidebar).rstrip() + "\n", encoding="utf-8")
+ (output / "_Sidebar.md").write_text(build_sidebar(root, titles), encoding="utf-8")
(output / "_Footer.md").write_text(
"Automatically synchronized from [langbot-app/langbot-wiki](https://github.com/langbot-app/langbot-wiki).\n",
encoding="utf-8",
diff --git a/tests/test_build_github_wiki.py b/tests/test_build_github_wiki.py
index 2aa6b52d6..bea84aecb 100644
--- a/tests/test_build_github_wiki.py
+++ b/tests/test_build_github_wiki.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import importlib.util
+import json
import tempfile
import unittest
from pathlib import Path, PurePosixPath
@@ -68,6 +69,66 @@ description: "ignored"
self.assertIn("[zh Guide](zh-guide)", (output / "_Sidebar.md").read_text())
self.assertTrue((output / "Home.md").is_file())
+ def test_sidebar_follows_docs_navigation_hierarchy(self) -> None:
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp)
+ documents = {
+ "zh/insight/guide.mdx": "---\ntitle: 中文首页\n---\nContent\n",
+ "zh/usage/models/readme.mdx": "---\ntitle: 模型配置\n---\nContent\n",
+ "zh/usage/models/custom.mdx": "---\ntitle: 自定义模型\n---\nContent\n",
+ "en/insight/guide.mdx": "---\ntitle: English Home\n---\nContent\n",
+ "ja/insight/guide.mdx": "---\ntitle: 日本語ホーム\n---\nContent\n",
+ }
+ for relative, content in documents.items():
+ path = root / relative
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(content, encoding="utf-8")
+ (root / "docs.json").write_text(
+ json.dumps(
+ {
+ "navigation": {
+ "languages": [
+ {
+ "language": "cn",
+ "tabs": [
+ {
+ "tab": "指南",
+ "groups": [
+ {"group": "快速开始", "pages": ["zh/insight/guide"]},
+ {
+ "group": "配置 AI",
+ "pages": [
+ "zh/usage/models/readme",
+ {
+ "group": "模型供应商",
+ "pages": ["zh/usage/models/custom"],
+ },
+ ],
+ },
+ ],
+ }
+ ],
+ }
+ ]
+ }
+ },
+ ensure_ascii=False,
+ ),
+ encoding="utf-8",
+ )
+
+ output = root / "out"
+ wiki.build(root, output)
+ sidebar = (output / "_Sidebar.md").read_text(encoding="utf-8")
+
+ self.assertIn("简体中文", sidebar)
+ self.assertIn("指南", sidebar)
+ self.assertIn("- **快速开始**", sidebar)
+ self.assertIn(" - [中文首页](zh-insight-guide)", sidebar)
+ self.assertIn(" - **模型供应商**", sidebar)
+ self.assertIn(" - [自定义模型](zh-usage-models-custom)", sidebar)
+ self.assertLess(sidebar.index("中文首页"), sidebar.index("模型配置"))
+
if __name__ == "__main__":
unittest.main()