Skip to content

Commit 149bfef

Browse files
committed
fix(docs): align navigation checks with repo conventions
Signed-off-by: Johnny Greco <jogreco@nvidia.com>
1 parent 7fb0c81 commit 149bfef

5 files changed

Lines changed: 92 additions & 18 deletions

File tree

‎.github/workflows/branch-docs.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ on:
77
- "fern/**"
88
- "mise.toml"
99
- "tasks/docs.toml"
10+
- "tasks/test.toml"
1011
- "tasks/scripts/check_docs_nav.py"
12+
- "tasks/scripts/check_docs_nav_test.py"
1113
- ".github/workflows/branch-docs.yml"
1214
- ".github/workflows/release-tag.yml"
1315

@@ -54,6 +56,9 @@ jobs:
5456
version: "0.10.12"
5557
python-version: "3.13"
5658

59+
- name: Test docs navigation check
60+
run: uv run --no-project --with pytest --with pytest-asyncio --with pyyaml pytest tasks/scripts/check_docs_nav_test.py
61+
5762
- name: Check docs navigation matches file paths
5863
run: uv run tasks/scripts/check_docs_nav.py
5964

‎architecture/build.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -584,8 +584,10 @@ See `CI.md` for the contributor workflow, labels, and maintainer merge-queue wor
584584
Published docs live in `docs/`. Navigation lives in `docs/index.yml`. Fern site
585585
configuration, components, theme assets, and publish settings live in `fern/`.
586586

587-
Use `mise run docs` for strict validation and `mise run docs:serve` for local
588-
preview. PR previews are produced by `.github/workflows/branch-docs.yml` when
587+
Use `mise run docs` for Fern validation and navigation-to-file-path consistency,
588+
and `mise run docs:serve` for local preview. The docs PR workflow also runs the
589+
navigation check's unit tests (`mise run test:docs-nav`).
590+
PR previews are produced by `.github/workflows/branch-docs.yml` when
589591
Fern credentials are available. Production docs publish from the release tag
590592
workflow.
591593

‎tasks/scripts/check_docs_nav.py‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,17 @@ def record(self, rel_path: str, url: str, how: str) -> None:
8484
if url != expected:
8585
self.issues.append(
8686
f"{rel_path}: URL {url} ({how}) does not match its file path "
87-
f"{expected}. Rename the file, change the nav label, or set "
88-
f'frontmatter slug: "{expected.lstrip("/")}".'
87+
f"{expected}. Rename the file, change the nav label, or set a "
88+
"relative slug in docs/index.yml. For folder-discovered pages, "
89+
f'use frontmatter slug: "{expected.lstrip("/")}".'
8990
)
9091

9192
def walk(self, items: list, prefix: str) -> None:
9293
for item in items or []:
9394
if "section" in item:
9495
url = prefix + "/" + item.get("slug", fern_slug(item["section"]))
96+
if item.get("skip-slug"):
97+
url = prefix
9598
if "path" in item:
9699
rel = item["path"]
97100
self.record(rel, self.page_url(rel, url), "section page")
@@ -103,7 +106,9 @@ def walk(self, items: list, prefix: str) -> None:
103106
f"docs/index.yml: nav page {label!r} points to missing {rel}"
104107
)
105108
continue
106-
if "slug" in item:
109+
if item.get("skip-slug"):
110+
default, how = prefix, "skip-slug"
111+
elif "slug" in item:
107112
default, how = prefix + "/" + item["slug"], "nav slug"
108113
else:
109114
default, how = (
@@ -126,6 +131,8 @@ def walk(self, items: list, prefix: str) -> None:
126131
+ "/"
127132
+ item.get("slug", fern_slug(item.get("title", folder)))
128133
)
134+
if item.get("skip-slug"):
135+
url = prefix
129136
for page in sorted((self.docs_root / folder).rglob("*.mdx")):
130137
rel = page.relative_to(self.docs_root).as_posix()
131138
stem = (
@@ -197,7 +204,9 @@ def run(repo_root: Path) -> list[str]:
197204

198205

199206
def main() -> int:
200-
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
207+
parser = argparse.ArgumentParser(
208+
description="Check that the published docs navigation mirrors the docs folder structure."
209+
)
201210
parser.add_argument(
202211
"--repo-root", type=Path, default=Path(__file__).resolve().parents[2]
203212
)

‎tasks/scripts/check_docs_nav_test.py‎

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,14 @@
33

44
from __future__ import annotations
55

6-
import importlib.util
7-
import sys
86
import textwrap
9-
from pathlib import Path
7+
from typing import TYPE_CHECKING
108

9+
import check_docs_nav
1110
import pytest
1211

13-
SPEC = importlib.util.spec_from_file_location(
14-
"check_docs_nav", Path(__file__).with_name("check_docs_nav.py")
15-
)
16-
check_docs_nav = importlib.util.module_from_spec(SPEC)
17-
sys.modules[SPEC.name] = check_docs_nav
18-
assert SPEC.loader is not None
19-
SPEC.loader.exec_module(check_docs_nav)
12+
if TYPE_CHECKING:
13+
from pathlib import Path
2014

2115
FERN_CONFIG = """\
2216
instances:
@@ -97,7 +91,7 @@ def test_label_url_that_differs_from_file_path_fails(tmp_path: Path) -> None:
9791
issues = check_docs_nav.run(root)
9892
assert len(issues) == 1
9993
assert "URL /sdk/type-script" in issues[0]
100-
assert 'slug: "sdk/typescript"' in issues[0]
94+
assert "relative slug in docs/index.yml" in issues[0]
10195

10296

10397
def test_frontmatter_slug_can_pin_the_url_to_the_file_path(tmp_path: Path) -> None:
@@ -135,6 +129,70 @@ def test_sidebar_name_must_match_nav_label(tmp_path: Path) -> None:
135129
]
136130

137131

132+
@pytest.mark.parametrize(
133+
("nav", "rel"),
134+
[
135+
(
136+
"""
137+
navigation:
138+
- section: Guides
139+
slug: ignored
140+
skip-slug: true
141+
contents:
142+
- page: Setup
143+
path: setup.mdx
144+
""",
145+
"setup.mdx",
146+
),
147+
(
148+
"""
149+
navigation:
150+
- section: Guides
151+
contents:
152+
- page: Setup
153+
slug: ignored
154+
skip-slug: true
155+
path: guides/index.mdx
156+
""",
157+
"guides/index.mdx",
158+
),
159+
(
160+
"""
161+
navigation:
162+
- section: Guides
163+
contents:
164+
- folder: guides
165+
slug: ignored
166+
skip-slug: true
167+
""",
168+
"guides/setup.mdx",
169+
),
170+
],
171+
)
172+
def test_skip_slug_omits_navigation_level(tmp_path: Path, nav: str, rel: str) -> None:
173+
root = build(tmp_path, nav, {rel: page("Setup")})
174+
assert check_docs_nav.run(root) == []
175+
176+
177+
def test_skip_slug_detects_url_that_differs_from_file_path(tmp_path: Path) -> None:
178+
root = build(
179+
tmp_path,
180+
"""
181+
navigation:
182+
- section: Guides
183+
skip-slug: true
184+
contents:
185+
- page: Setup
186+
path: guides/setup.mdx
187+
""",
188+
{"guides/setup.mdx": page("Setup")},
189+
)
190+
issues = check_docs_nav.run(root)
191+
assert len(issues) == 1
192+
assert "URL /setup" in issues[0]
193+
assert "does not match its file path /guides/setup" in issues[0]
194+
195+
138196
def test_folder_pages_use_file_names_and_orphans_fail(tmp_path: Path) -> None:
139197
root = build(
140198
tmp_path,

‎tasks/test.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ run = "uv run --no-project --with pytest --with pytest-asyncio --with pyyaml pyt
3131

3232
["test:docs-nav"]
3333
description = "Test the docs navigation check"
34-
run = "uv run --no-project --with pytest --with pyyaml pytest tasks/scripts/check_docs_nav_test.py"
34+
run = "uv run --no-project --with pytest --with pytest-asyncio --with pyyaml pytest tasks/scripts/check_docs_nav_test.py"
3535

3636
["test:sbom"]
3737
description = "Run SBOM tooling tests"

0 commit comments

Comments
 (0)