Skip to content

Commit d479b22

Browse files
committed
docs: fail the build on pages missing from the nav
MkDocs (--strict with validation.omitted_files: warn) failed the build for a page on disk that the nav omitted; Zensical builds it as unreachable orphan HTML. Validate both directions of nav/page drift in build_config with one nav walker, using the same scheme-based external-link test as llms_txt so a scheme-only nav entry (mailto:) cannot be misread as a page path. The generated api/ tree is exempt from the orphan check: its nav is spliced in from the same generator that writes the files.
1 parent 029a47c commit d479b22

1 file changed

Lines changed: 35 additions & 14 deletions

File tree

scripts/docs/build_config.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from __future__ import annotations
1414

15+
import re
1516
from pathlib import Path
1617

1718
# Both scripts live in this directory, which Python puts on sys.path[0] when
@@ -21,23 +22,44 @@
2122

2223
ROOT = Path(__file__).parent.parent.parent
2324

25+
# A scheme-prefixed nav value (https:, mailto:, ...) is an external link, not
26+
# a page path (same classifier as llms_txt.py; a `://` test would misread
27+
# scheme-only URIs as pages).
28+
_EXTERNAL = re.compile(r"[a-zA-Z][a-zA-Z0-9+.-]*:")
2429

25-
def _missing_nav_pages(nav: list, docs_dir: Path) -> list[str]:
26-
"""Collect nav page references that don't exist under the docs dir.
2730

28-
Zensical (0.0.48) ships a nav entry for a nonexistent page as a broken
29-
link without any diagnostic, even under --strict — MkDocs aborted the
30-
build. Validating here keeps that guarantee: the concrete config never
31-
leaves this script referencing a page that isn't there.
32-
"""
33-
missing: list[str] = []
31+
def _nav_pages(nav: list) -> set[str]:
32+
"""Collect every local page reference in the nav (external links excluded)."""
33+
pages: set[str] = set()
3434
for entry in nav:
3535
value = next(iter(entry.values())) if isinstance(entry, dict) else entry
3636
if isinstance(value, list):
37-
missing.extend(_missing_nav_pages(value, docs_dir))
38-
elif "://" not in value and not (docs_dir / value).is_file():
39-
missing.append(value)
40-
return missing
37+
pages |= _nav_pages(value)
38+
elif not _EXTERNAL.match(value):
39+
pages.add(value)
40+
return pages
41+
42+
43+
def _validate_nav(nav: list, docs_dir: Path) -> None:
44+
"""Fail on nav/page drift in either direction.
45+
46+
Zensical (0.0.48) ships a nav entry for a nonexistent page as a broken
47+
link without any diagnostic even under --strict, and publishes a page
48+
that no nav entry reaches as unreachable orphan HTML; MkDocs aborted the
49+
build on both (--strict with `validation.omitted_files: warn`).
50+
Validating here keeps those guarantees. The generated `api/` tree is
51+
exempt from the orphan check: its nav is spliced in from the same
52+
generator that writes the files, so it cannot drift.
53+
"""
54+
pages = _nav_pages(nav)
55+
if missing := sorted(page for page in pages if not (docs_dir / page).is_file()):
56+
raise SystemExit(f"build_config: nav references pages that don't exist under docs/: {missing}")
57+
# Dot-directories (e.g. `.overrides` theme files) are not pages: the site
58+
# builder ignores them, so the orphan check must too.
59+
relative = (page.relative_to(docs_dir) for page in docs_dir.rglob("*.md"))
60+
on_disk = {page.as_posix() for page in relative if not any(part.startswith(".") for part in page.parts)}
61+
if orphaned := sorted(page for page in on_disk - pages if not page.startswith("api/")):
62+
raise SystemExit(f"build_config: pages under docs/ that no nav entry reaches: {orphaned}")
4163

4264

4365
def build_config() -> None:
@@ -53,8 +75,7 @@ def build_config() -> None:
5375
else:
5476
raise SystemExit("build_config: no 'API Reference' entry found in mkdocs.yml nav")
5577

56-
if missing := _missing_nav_pages(config["nav"], ROOT / "docs"):
57-
raise SystemExit(f"build_config: nav references pages that don't exist under docs/: {missing}")
78+
_validate_nav(config["nav"], ROOT / "docs")
5879

5980
output = ROOT / "mkdocs.gen.yml"
6081
output.write_text(yaml.safe_dump(config, sort_keys=False, allow_unicode=True), encoding="utf-8")

0 commit comments

Comments
 (0)