From b39ddf44ba9cc7ad7fe481bafbf1e5d2a592ed16 Mon Sep 17 00:00:00 2001 From: Samran Asif Date: Wed, 9 Sep 2026 16:48:01 +0500 Subject: [PATCH] fix(docs): the published docs site had empty pages, and the build stayed green Every `--8<--` include in this site was silently resolving to nothing. All nine used a `../` path -- `--8<-- "../ARCHITECTURE.md"` and friends -- which pymdownx.snippets refuses because it escapes `base_path`, and `check_paths: false` turned that refusal into silence. `mkdocs build --strict` then passed with nothing to say. The result is live: https://webdevsamran.github.io/devrepro-doctor/ has been serving an Architecture page of 68 words, all of it navigation chrome, and the same for Roadmap, Contributing, Security, Interop, Product gaps, Changelog and Code of conduct. The home page was missing its README section. Found while porting this docs setup to api-verity-lab: the same include pattern failed there, and rather than work around it I checked whether it had ever worked here. It had not. Fixed by pointing `base_path` at the repository root and dropping the `../` from every include, so the paths stay inside base_path. `check_paths` is now true, which makes a missing snippet a build failure rather than a silent skip. `validation.links.not_found` is downgraded to info, deliberately and with the reason recorded in mkdocs.yml: included root documents carry links written relative to the repository, where they are correct and where most people read them, but which sit outside the docs tree inside the site. That check is cosmetic here; `check_paths` is the one that matters, and it is now loud. scripts/check_docs_site.py asserts the built site has content rather than merely building. A page under 120 words is chrome, not a document -- empty pages measured 68, real ones 383 to 655. Verified by reintroducing the `../` path and watching the build fail. 16 pages now build with real content in all of them. --- .github/workflows/docs.yml | 5 ++ docs/architecture.md | 2 +- docs/changelog.md | 2 +- docs/code-of-conduct.md | 2 +- docs/contributing.md | 2 +- docs/index.md | 2 +- docs/interop.md | 2 +- docs/product-gaps.md | 2 +- docs/roadmap.md | 2 +- docs/security.md | 2 +- mkdocs.yml | 19 +++++++- scripts/check_docs_site.py | 94 ++++++++++++++++++++++++++++++++++++++ 12 files changed, 125 insertions(+), 11 deletions(-) create mode 100644 scripts/check_docs_site.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d9a81e8..ebb7f43 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -33,6 +33,11 @@ jobs: run: pip install -r requirements-docs.txt - name: Build (strict) run: mkdocs build --strict --site-dir site + - name: Docs site actually contains its documents + # `mkdocs build --strict` succeeded for months while publishing empty + # pages: all nine snippet includes used a `../` path that pymdownx + # refuses, and check_paths:false made the refusal silent. + run: python scripts/check_docs_site.py --site site - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: site diff --git a/docs/architecture.md b/docs/architecture.md index a7815ed..a80de5b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1 +1 @@ ---8<-- "../ARCHITECTURE.md" +--8<-- "ARCHITECTURE.md" diff --git a/docs/changelog.md b/docs/changelog.md index 11fa8a5..786b75d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1 +1 @@ ---8<-- "../CHANGELOG.md" +--8<-- "CHANGELOG.md" diff --git a/docs/code-of-conduct.md b/docs/code-of-conduct.md index d468a9d..01f2ea2 100644 --- a/docs/code-of-conduct.md +++ b/docs/code-of-conduct.md @@ -1 +1 @@ ---8<-- "../CODE_OF_CONDUCT.md" +--8<-- "CODE_OF_CONDUCT.md" diff --git a/docs/contributing.md b/docs/contributing.md index 3f2d90d..ea38c9b 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1 +1 @@ ---8<-- "../CONTRIBUTING.md" +--8<-- "CONTRIBUTING.md" diff --git a/docs/index.md b/docs/index.md index 1ed80e8..d84327e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -39,4 +39,4 @@ devrepro doctor # full read-only diagnostic scan - [Interoperability](interop.md) — how we relate to Nix, mise, Devbox, devenv… - [Roadmap](roadmap.md) — where we're going ---8<-- "../README.md" +--8<-- "README.md" diff --git a/docs/interop.md b/docs/interop.md index 7038158..8592436 100644 --- a/docs/interop.md +++ b/docs/interop.md @@ -1 +1 @@ ---8<-- "../INTEROP.md" +--8<-- "INTEROP.md" diff --git a/docs/product-gaps.md b/docs/product-gaps.md index 7d0add4..9a9e8a5 100644 --- a/docs/product-gaps.md +++ b/docs/product-gaps.md @@ -1 +1 @@ ---8<-- "../PRODUCT_GAPS.md" +--8<-- "PRODUCT_GAPS.md" diff --git a/docs/roadmap.md b/docs/roadmap.md index 57aad72..40fbfee 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1 +1 @@ ---8<-- "../ROADMAP.md" +--8<-- "ROADMAP.md" diff --git a/docs/security.md b/docs/security.md index b6cb6d8..a8ada70 100644 --- a/docs/security.md +++ b/docs/security.md @@ -1 +1 @@ ---8<-- "../SECURITY.md" +--8<-- "SECURITY.md" diff --git a/mkdocs.yml b/mkdocs.yml index 1ddee8b..3826d11 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -52,8 +52,8 @@ markdown_extensions: - admonition - pymdownx.superfences - pymdownx.snippets: - base_path: docs - check_paths: false + base_path: . + check_paths: true - tables - toc: permalink: true @@ -61,3 +61,18 @@ markdown_extensions: validation: omitted_files: warn absolute_links: warn + links: + # Root documents (README, ARCHITECTURE, CONTRIBUTING...) are included into + # this site by snippet and carry links written relative to the repository + # root, where they are correct and where most people read them. Inside the + # site those targets sit outside the docs tree, so mkdocs reports them as + # not-found and --strict aborts. + # + # Downgraded to info deliberately, and paired with `check_paths: true` + # above, which is the check that actually matters: a snippet that cannot be + # found now fails the build. It previously did not. With check_paths off, + # all nine includes in this site silently resolved to nothing, and the + # published pages for Architecture, Roadmap, Contributing, Security, + # Interop, Product gaps, Changelog and Code of conduct were empty while the + # build stayed green. + not_found: info diff --git a/scripts/check_docs_site.py b/scripts/check_docs_site.py new file mode 100644 index 0000000..f47fb1b --- /dev/null +++ b/scripts/check_docs_site.py @@ -0,0 +1,94 @@ +"""Assert the built docs site actually contains its documents. + +`mkdocs build --strict` passing does not mean the site has content in it. This +project's site was green while publishing empty pages: every one of its nine +`--8<--` snippet includes used a `../` path, which pymdownx refuses because it +escapes `base_path`, and `check_paths: false` made that refusal silent. The +Architecture page shipped with 68 words of navigation chrome and none of +ARCHITECTURE.md. + +A build that succeeds while producing nothing is the same defect this +repository has fixed twice elsewhere -- a CI step named for a check it never +performed. So the site is checked for content, not just for exit status. + + python scripts/check_docs_site.py # build, then verify + python scripts/check_docs_site.py --site DIR # verify an existing build +""" + +from __future__ import annotations + +import argparse +import re +import subprocess +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent + +#: A page carrying only nav chrome lands around 60-70 words. Real documents +#: here run to hundreds. 120 sits clear of the chrome and below the shortest +#: genuine page, so it catches an empty include without tuning per page. +MIN_WORDS = 120 + +TAG = re.compile(r"<[^>]+>") + + +def words_in(html: str) -> int: + body = html.split("", 1)[0] + return len(TAG.sub(" ", body).split()) + + +def build(into: Path) -> None: + result = subprocess.run( # noqa: S603 - fixed argv, no shell, no user input + [sys.executable, "-m", "mkdocs", "build", "--strict", "--site-dir", str(into)], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + print(result.stderr or result.stdout, file=sys.stderr) + raise SystemExit("mkdocs build failed") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--site", type=Path, default=None) + args = parser.parse_args() + + with tempfile.TemporaryDirectory() as tmp: + site = args.site or Path(tmp) / "site" + if args.site is None: + build(site) + + pages = sorted(site.rglob("index.html")) + if not pages: + print("no pages were built", file=sys.stderr) + return 1 + + thin: list[tuple[str, int]] = [] + for page in pages: + name = page.parent.relative_to(site).as_posix() or "(home)" + if name.startswith(("assets", "search")): + continue + count = words_in(page.read_text(encoding="utf-8", errors="replace")) + if count < MIN_WORDS: + thin.append((name, count)) + + if thin: + print( + f"{len(thin)} page(s) built with almost no content -- a snippet " + "include is probably resolving to nothing:", + file=sys.stderr, + ) + for name, count in thin: + print(f" {name}: {count} words", file=sys.stderr) + return 1 + + print(f"ok {len(pages)} pages built, all carrying real content") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())