Skip to content

check links - #57

Open
bc1cindy wants to merge 2 commits into
mainfrom
check-links
Open

bc1cindy wants to merge 2 commits into
mainfrom
check-links

Conversation

@bc1cindy

@bc1cindy bc1cindy commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

links to missing files or anchors, and paths that leave the book, fail the build. lychee checks the site mdbook writes, offline, mounted under /docs/ where it is served, so a link from / fails too

links to the repository outside docs/ should be absolute URLs; offline, lychee does not check http(s) links

closes #54

stacked on #50


Captura de Tela 2026-10-15 às 17 12 22
Captura de Tela 2026-10-15 às 17 12 41
Captura de Tela 2026-10-15 às 17 13 15
Captura de Tela 2026-10-15 às 17 13 38
Captura de Tela 2026-10-15 às 17 13 58
Captura de Tela 2026-10-15 às 17 14 29
Captura de Tela 2026-10-15 às 17 14 46

@bc1cindy
bc1cindy added this pull request to stack #58 October 15, 2026 20:41
@bc1cindy bc1cindy mentioned this pull request Oct 15, 2026

@yuval-block yuval-block left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think i overcomplicated it

  • let's just add static stubs in a new PR, and each linked PR will rebase on top of it and overwrite the stub
  • and make this PR about checking for no broken links, with the stubs in place we can already merge it instead of waiting for the open PRs

Comment thread review.toml Outdated
@github-actions

Copy link
Copy Markdown
range-diff for force push 48c9aed895 → fc11f53833 October 15, 2026 22:54 UTC: 2 modified, 2 dropped, 1 added

reproduce: git range-diff fee4b8c..48c9aed fee4b8c..fc11f53

1:  e4eb92ac1d < -:  ---------- Stand in for chapters that are not merged yet
-:  ---------- > 1:  30817a4730 Check links to missing files
2:  a68c3909fa ! 2:  ca2b6c8fdc Check the anchors of links
3:  38805e47f7 ! 3:  fc11f53833 Check the paths in HTML
4:  48c9aed895 < -:  ---------- Print build logs in CI flake checks
2: a68c3909fa ! 2: ca2b6c8fdc Check the anchors of links
@@ Commit message
     silently opens the page at its top.
 
  ## contrib/book.py ##
-@@ contrib/book.py: and then dropped, as mdbook would render it as text, and links to what the
- book leaves out point at the repository at the built revision. A link to a
- page of docs/ that does not exist yet leads to a chapter saying it is under
- review, with the pull request and title review.toml gives it, if any; a
--link to any other missing file is an error. The bundle's other files are
--copied as they are.
-+link to any other missing file, or to an anchor a page of the book lacks,
-+is an error. The bundle's other files are copied as they are.
+@@ contrib/book.py: README is the introduction, and the index, with its descriptions, is the
+ contents page to which links to docs/index.md lead. Frontmatter is checked
+ and then dropped, as mdbook would render it as text, and links to what the
+ book leaves out point at the repository at the built revision. A link to
+-a missing file is an error. The bundle's other files are copied as they
+-are.
++a missing file, or to an anchor a page of the book lacks, is an error. The
++bundle's other files are copied as they are.
  
      python3 book.py ROOT OUT --repository URL --rev REV
  """
@@ contrib/book.py: def assemble(root, out, repository, rev):
 +        for target, _ in found
 +        if target and not (root / target).exists()
      }
-     reviewed = root / "review.toml"
-     listed = tomllib.loads(reviewed.read_text()) if reviewed.exists() else {}
-@@ contrib/book.py: def assemble(root, out, repository, rev):
-         raise ValueError(f"links to missing files: {broken}")
-     if unlisted := sorted(review.keys() - listed.keys()):
-         print(f"stubs for pages review.toml does not list: {unlisted}", file=sys.stderr)
--    chapters = entries(index) | review.keys()
-+    indexed = entries(index)
-+    chapters = indexed | review.keys()
+     if missing:
+         raise ValueError(f"links to missing files: {sorted(missing)}")
+     chapters = entries(index)
 +    ids = {
 +        name: anchors(text)
 +        for name, (_, text) in sources.items()
-+        if name in indexed or name in ("README.md", CONTENTS)
++        if name in chapters or name in ("README.md", CONTENTS)
 +    }
 +    dangling = []
 +    for name, found in links.items():
@@ contrib/book.py: def assemble(root, out, repository, rev):
 +        raise ValueError(f"links to missing anchors: {sorted(dangling)}")
      out.mkdir(parents=True, exist_ok=True)
      shutil.copytree(docs, out, ignore=shutil.ignore_patterns("*.md"), dirs_exist_ok=True)
-     (out / "SUMMARY.md").write_text(summary(index, pages, review))
+     (out / "SUMMARY.md").write_text(summary(index, pages))
 
  ## contrib/test_book.py ##
 @@ contrib/test_book.py: def test_indented_text_that_is_not_code_is_relinked(text):
@@ contrib/test_book.py: def test_indented_text_that_is_not_code_is_relinked(text):
  
  
  def test_entries_are_the_chapters_the_index_names():
-@@ contrib/test_book.py: def test_assembly_rejects_review_toml_listing_a_page_that_exists(tmp_path):
-     error = r"review.toml lists pages that exist: \['later.md'\]"
-     with pytest.raises(ValueError, match=error):
+@@ contrib/test_book.py: def test_assembly_rejects_a_link_to_a_missing_file(tmp_path, link):
+     with pytest.raises(ValueError, match="links to missing files: .*one.md"):
          book.assemble(root, tmp_path / "src", "https://example.org", "abc")
-+
+ 
 +
 +def chapter_linking(tmp_path, link):
 +    root = tmp_path / "root"
@@ contrib/test_book.py: def test_assembly_rejects_review_toml_listing_a_page_that_
 +
 +@pytest.mark.parametrize(
 +    "link",
-+    ["#detail", "two.md#two", "index.md#part", "/one.md#one", "later.md#a", "left-out.md#x"],
++    ["#detail", "two.md#two", "index.md#part", "/one.md#one", "left-out.md#x"],
 +)
 +def test_assembly_accepts_anchors_it_cannot_or_does_not_miss(tmp_path, link):
 +    root = chapter_linking(tmp_path, link)
3: 38805e47f7 ! 3: fc11f53833 Check the paths in HTML
@@ Commit message
     page of docs/ to a file in the book.
 
  ## contrib/book.py ##
-@@ contrib/book.py: book leaves out point at the repository at the built revision. A link to a
- page of docs/ that does not exist yet leads to a chapter saying it is under
- review, with the pull request and title review.toml gives it, if any; a
- link to any other missing file, or to an anchor a page of the book lacks,
--is an error. The bundle's other files are copied as they are.
-+is an error. Paths in HTML are left as written, so they must lead from a
-+page of docs/ to a file the book has. The bundle's other files are copied
-+as they are.
+@@ contrib/book.py: README is the introduction, and the index, with its descriptions, is the
+ contents page to which links to docs/index.md lead. Frontmatter is checked
+ and then dropped, as mdbook would render it as text, and links to what the
+ book leaves out point at the repository at the built revision. A link to
+-a missing file, or to an anchor a page of the book lacks, is an error. The
+-bundle's other files are copied as they are.
++a missing file, or to an anchor a page of the book lacks, is an error.
++Paths in HTML are left as written, so they must lead from a page of docs/
++to a file the book has. The bundle's other files are copied as they are.
  
      python3 book.py ROOT OUT --repository URL --rev REV
  """
@@ contrib/book.py: def assemble(root, out, repository, rev):
          (name, target)
          for name, found in links.items()
 @@ contrib/book.py: def assemble(root, out, repository, rev):
-         print(f"stubs for pages review.toml does not list: {unlisted}", file=sys.stderr)
-     indexed = entries(index)
-     chapters = indexed | review.keys()
+     if missing:
+         raise ValueError(f"links to missing files: {sorted(missing)}")
+     chapters = entries(index)
 +    if strays := sorted(
 +        (name, target)
 +        for name, found in html.items()

@bc1cindy bc1cindy changed the title check links and stand in for chapters in review check links Oct 15, 2026
@bc1cindy

Copy link
Copy Markdown
Member Author

thanks

done, stubs are in #59 and this pr now only checks links

I'll open a ticket per stub when #59 merges

Captura de Tela 2026-10-15 às 20 20 01

wdyt?

@github-actions

Copy link
Copy Markdown
range-diff for force push fc11f53833 → 5ceb07095a October 16, 2026 02:14 UTC: 3 modified, 2 dropped

reproduce: git range-diff e0bd68f..fc11f53 2652d48..5ceb070

1:  80996f51fe < -:  ---------- Generate SUMMARY.md from an OKF index.md
2:  fee4b8c9c4 < -:  ---------- Check the status and stale_after of chapters
3:  30817a4730 ! 1:  ed2c0a76d2 Check links to missing files
4:  ca2b6c8fdc ! 2:  369fafa1b4 Check the anchors of links
5:  fc11f53833 ! 3:  5ceb07095a Check the paths in HTML
3: 30817a4730 ! 1: ed2c0a76d2 Check links to missing files
@@ contrib/book.py: without their descriptions, so an entry must name a chapter. Th
      python3 book.py ROOT OUT --repository URL --rev REV
  """
 @@ contrib/book.py: def relink(text, base, blob, chapters):
-             return f"{prefix}{blob}/{target}{sep}{frag}"
-         return f"{prefix}{posixpath.relpath(target, posixpath.join(DOCS, here))}{sep}{frag}"
+         target = posixpath.relpath(target, posixpath.join(DOCS, here))
+         return f"{prefix}{target}{sep}{frag}{suffix}"
  
 +    return rewrite(text, sub)
 +
@@ contrib/book.py: def relink(text, base, blob, chapters):
 +
 +    def sub(m):
 +        if not m["tick"] and not SCHEME.match(m["url"]):
-+            found.add(resolve(base, m["url"].partition("#")[0]))
++            found.add(resolve(base, decode(m["url"].partition("#")[0])))
 +        return m[0]
 +
 +    rewrite(text, sub)
@@ contrib/test_book.py: def test_assembly_links_a_page_left_out_of_the_index_to_th
 +    with pytest.raises(ValueError, match="links to missing files: .*one.md"):
 +        book.assemble(root, tmp_path / "src", "https://example.org", "abc")
 +
++
++@pytest.mark.parametrize("link, name", [(r"a\).md", "a).md"), ("my%20file.md", "my file.md")])
++def test_assembly_accepts_a_link_that_encodes_its_file(tmp_path, link, name):
++    root = tmp_path / "root"
++    (root / "docs").mkdir(parents=True)
++    (root / "README.md").write_text("# Intro\n")
++    (root / "docs" / "index.md").write_text("# Part\n")
++    page = "---\ntype: Chapter\nstatus: stable\n---\n\n"
++    (root / "docs" / "one.md").write_text(f"{page}[x]({link})\n")
++    (root / "docs" / name).write_text(page)
++    book.assemble(root, tmp_path / "src", "https://example.org", "abc")
++
4: ca2b6c8fdc ! 2: 369fafa1b4 Check the anchors of links
@@ contrib/book.py: README is the introduction, and the index, with its description
      python3 book.py ROOT OUT --repository URL --rev REV
  """
 @@ contrib/book.py: LINK = re.compile(
- # written and a code span runs within one block.
+ # left as written and a code span runs within one block.
  MARKDOWN = MarkdownIt("commonmark").enable("table").use(deflist_plugin).use(footnote_plugin)
  SCHEME = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*:|#")
 +ATTRIBUTES = re.compile(r"\s*\{([^}]*)\}\s*$")
@@ contrib/book.py: def relink(text, base, blob, chapters):
  
      def sub(m):
 -        if not m["tick"] and not SCHEME.match(m["url"]):
--            found.add(resolve(base, m["url"].partition("#")[0]))
+-            found.add(resolve(base, decode(m["url"].partition("#")[0])))
 +        url = m["url"]
 +        if not m["tick"] and (url.startswith("#") or not SCHEME.match(url)):
 +            path, _, frag = url.partition("#")
-+            found.add((resolve(base, path) if path else None, frag))
++            found.add((resolve(base, decode(path)) if path else None, frag))
          return m[0]
  
      rewrite(text, sub)
@@ contrib/test_book.py: def test_indented_text_that_is_not_code_is_relinked(text):
  
  
  def test_entries_are_the_chapters_the_index_names():
-@@ contrib/test_book.py: def test_assembly_rejects_a_link_to_a_missing_file(tmp_path, link):
-     with pytest.raises(ValueError, match="links to missing files: .*one.md"):
-         book.assemble(root, tmp_path / "src", "https://example.org", "abc")
+@@ contrib/test_book.py: def test_assembly_accepts_a_link_that_encodes_its_file(tmp_path, link, name):
+     (root / "docs" / name).write_text(page)
+     book.assemble(root, tmp_path / "src", "https://example.org", "abc")
  
 +
 +def chapter_linking(tmp_path, link):
5: fc11f53833 ! 3: 5ceb07095a Check the paths in HTML
@@ Commit message
     Check the paths in HTML
 
     mdbook passes HTML as written, so src, srcset and href must lead from a
-    page of docs/ to a file in the book.
+    page of docs/ to a file in the book. Markdown links inside HTML blocks
+    are text to mdbook, so only HTML_LINK reads those blocks.
 
  ## contrib/book.py ##
 @@ contrib/book.py: README is the introduction, and the index, with its descriptions, is the
@@ contrib/book.py: README is the introduction, and the index, with its description
      python3 book.py ROOT OUT --repository URL --rev REV
  """
 @@ contrib/book.py: LINK = re.compile(
-     r"|(?P<prefix>\]\(|" + CONTAINER + r"\[[^\]^][^\]]*\]:[ \t]*)(?P<url>[^)\s]+)",
+     r"(?P<suffix>(?(angle)>))",
      re.MULTILINE | re.DOTALL,
  )
+-# The blocks of a page as mdbook parses them, so that code and HTML blocks are
+-# left as written and a code span runs within one block.
 +HTML_LINK = re.compile(
 +    r"(?P<tick>`+).+?(?P=tick)"
 +    r"|\b(?P<attr>src|srcset|href)\s*=\s*(?P<quote>[\"'])(?P<url>.*?)(?P=quote)",
 +    re.DOTALL | re.IGNORECASE,
 +)
- # The blocks of a page as mdbook parses them, so that code blocks are left as
- # written and a code span runs within one block.
++# The blocks of a page as mdbook parses them, so that links are read only where
++# mdbook reads them and a code span runs within one block.
  MARKDOWN = MarkdownIt("commonmark").enable("table").use(deflist_plugin).use(footnote_plugin)
++# The blocks in which mdbook reads no link of each kind.
++VERBATIM = {LINK: ("fence", "code_block", "html_block"), HTML_LINK: ("fence", "code_block")}
+ SCHEME = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*:|#")
+ ATTRIBUTES = re.compile(r"\s*\{([^}]*)\}\s*$")
+ HTML_ID = re.compile(r"""\bid\s*=\s*["']([^"']+)["']""")
 @@ contrib/book.py: def relink(text, base, blob, chapters):
      return rewrite(text, sub)
  
@@ contrib/book.py: def relink(text, base, blob, chapters):
 -        url = m["url"]
 -        if not m["tick"] and (url.startswith("#") or not SCHEME.match(url)):
 -            path, _, frag = url.partition("#")
--            found.add((resolve(base, path) if path else None, frag))
+-            found.add((resolve(base, decode(path)) if path else None, frag))
 +        if m["tick"]:
 +            return m[0]
 +        urls = [m["url"]]
@@ contrib/book.py: def relink(text, base, blob, chapters):
 +        for url in urls:
 +            if url.startswith("#") or not SCHEME.match(url):
 +                path, _, frag = url.partition("#")
-+                found.add((resolve(base, path) if path else None, frag))
++                found.add((resolve(base, decode(path)) if path else None, frag))
          return m[0]
  
 -    rewrite(text, sub)
@@ contrib/book.py: def anchors(text):
 -def rewrite(text, sub):
 -    """`text` with `sub` applied to each match of LINK outside code."""
 +def rewrite(text, sub, link=LINK):
-+    """`text` with `sub` applied to each match of `link` outside code."""
++    """`text` with `sub` applied to each match of `link` outside the blocks
++    in which mdbook does not read it."""
      lines = text.splitlines(keepends=True)
      blocks = {
          t.map[0]: t
-@@ contrib/book.py: def rewrite(text, sub):
+         for t in MARKDOWN.parse(text)
+-        if t.type in ("fence", "code_block", "html_block", "inline") and t.map
++        if t.type in (*VERBATIM[link], "inline") and t.map
+     }
+     out, i = [], 0
+     while i < len(lines):
          block = blocks.get(i)
          end = max(block.map[1], i + 1) if block else i + 1
          chunk = "".join(lines[i:end])

@github-actions

Copy link
Copy Markdown
range-diff for force push fc11f53833 → 5ceb07095a October 16, 2026 02:14 UTC: 3 modified, 2 dropped

reproduce: git range-diff e0bd68f..fc11f53 2652d48..5ceb070

1:  80996f51fe < -:  ---------- Generate SUMMARY.md from an OKF index.md
2:  fee4b8c9c4 < -:  ---------- Check the status and stale_after of chapters
3:  30817a4730 ! 1:  ed2c0a76d2 Check links to missing files
4:  ca2b6c8fdc ! 2:  369fafa1b4 Check the anchors of links
5:  fc11f53833 ! 3:  5ceb07095a Check the paths in HTML
3: 30817a4730 ! 1: ed2c0a76d2 Check links to missing files
@@ contrib/book.py: without their descriptions, so an entry must name a chapter. Th
      python3 book.py ROOT OUT --repository URL --rev REV
  """
 @@ contrib/book.py: def relink(text, base, blob, chapters):
-             return f"{prefix}{blob}/{target}{sep}{frag}"
-         return f"{prefix}{posixpath.relpath(target, posixpath.join(DOCS, here))}{sep}{frag}"
+         target = posixpath.relpath(target, posixpath.join(DOCS, here))
+         return f"{prefix}{target}{sep}{frag}{suffix}"
  
 +    return rewrite(text, sub)
 +
@@ contrib/book.py: def relink(text, base, blob, chapters):
 +
 +    def sub(m):
 +        if not m["tick"] and not SCHEME.match(m["url"]):
-+            found.add(resolve(base, m["url"].partition("#")[0]))
++            found.add(resolve(base, decode(m["url"].partition("#")[0])))
 +        return m[0]
 +
 +    rewrite(text, sub)
@@ contrib/test_book.py: def test_assembly_links_a_page_left_out_of_the_index_to_th
 +    with pytest.raises(ValueError, match="links to missing files: .*one.md"):
 +        book.assemble(root, tmp_path / "src", "https://example.org", "abc")
 +
++
++@pytest.mark.parametrize("link, name", [(r"a\).md", "a).md"), ("my%20file.md", "my file.md")])
++def test_assembly_accepts_a_link_that_encodes_its_file(tmp_path, link, name):
++    root = tmp_path / "root"
++    (root / "docs").mkdir(parents=True)
++    (root / "README.md").write_text("# Intro\n")
++    (root / "docs" / "index.md").write_text("# Part\n")
++    page = "---\ntype: Chapter\nstatus: stable\n---\n\n"
++    (root / "docs" / "one.md").write_text(f"{page}[x]({link})\n")
++    (root / "docs" / name).write_text(page)
++    book.assemble(root, tmp_path / "src", "https://example.org", "abc")
++
4: ca2b6c8fdc ! 2: 369fafa1b4 Check the anchors of links
@@ contrib/book.py: README is the introduction, and the index, with its description
      python3 book.py ROOT OUT --repository URL --rev REV
  """
 @@ contrib/book.py: LINK = re.compile(
- # written and a code span runs within one block.
+ # left as written and a code span runs within one block.
  MARKDOWN = MarkdownIt("commonmark").enable("table").use(deflist_plugin).use(footnote_plugin)
  SCHEME = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*:|#")
 +ATTRIBUTES = re.compile(r"\s*\{([^}]*)\}\s*$")
@@ contrib/book.py: def relink(text, base, blob, chapters):
  
      def sub(m):
 -        if not m["tick"] and not SCHEME.match(m["url"]):
--            found.add(resolve(base, m["url"].partition("#")[0]))
+-            found.add(resolve(base, decode(m["url"].partition("#")[0])))
 +        url = m["url"]
 +        if not m["tick"] and (url.startswith("#") or not SCHEME.match(url)):
 +            path, _, frag = url.partition("#")
-+            found.add((resolve(base, path) if path else None, frag))
++            found.add((resolve(base, decode(path)) if path else None, frag))
          return m[0]
  
      rewrite(text, sub)
@@ contrib/test_book.py: def test_indented_text_that_is_not_code_is_relinked(text):
  
  
  def test_entries_are_the_chapters_the_index_names():
-@@ contrib/test_book.py: def test_assembly_rejects_a_link_to_a_missing_file(tmp_path, link):
-     with pytest.raises(ValueError, match="links to missing files: .*one.md"):
-         book.assemble(root, tmp_path / "src", "https://example.org", "abc")
+@@ contrib/test_book.py: def test_assembly_accepts_a_link_that_encodes_its_file(tmp_path, link, name):
+     (root / "docs" / name).write_text(page)
+     book.assemble(root, tmp_path / "src", "https://example.org", "abc")
  
 +
 +def chapter_linking(tmp_path, link):
5: fc11f53833 ! 3: 5ceb07095a Check the paths in HTML
@@ Commit message
     Check the paths in HTML
 
     mdbook passes HTML as written, so src, srcset and href must lead from a
-    page of docs/ to a file in the book.
+    page of docs/ to a file in the book. Markdown links inside HTML blocks
+    are text to mdbook, so only HTML_LINK reads those blocks.
 
  ## contrib/book.py ##
 @@ contrib/book.py: README is the introduction, and the index, with its descriptions, is the
@@ contrib/book.py: README is the introduction, and the index, with its description
      python3 book.py ROOT OUT --repository URL --rev REV
  """
 @@ contrib/book.py: LINK = re.compile(
-     r"|(?P<prefix>\]\(|" + CONTAINER + r"\[[^\]^][^\]]*\]:[ \t]*)(?P<url>[^)\s]+)",
+     r"(?P<suffix>(?(angle)>))",
      re.MULTILINE | re.DOTALL,
  )
+-# The blocks of a page as mdbook parses them, so that code and HTML blocks are
+-# left as written and a code span runs within one block.
 +HTML_LINK = re.compile(
 +    r"(?P<tick>`+).+?(?P=tick)"
 +    r"|\b(?P<attr>src|srcset|href)\s*=\s*(?P<quote>[\"'])(?P<url>.*?)(?P=quote)",
 +    re.DOTALL | re.IGNORECASE,
 +)
- # The blocks of a page as mdbook parses them, so that code blocks are left as
- # written and a code span runs within one block.
++# The blocks of a page as mdbook parses them, so that links are read only where
++# mdbook reads them and a code span runs within one block.
  MARKDOWN = MarkdownIt("commonmark").enable("table").use(deflist_plugin).use(footnote_plugin)
++# The blocks in which mdbook reads no link of each kind.
++VERBATIM = {LINK: ("fence", "code_block", "html_block"), HTML_LINK: ("fence", "code_block")}
+ SCHEME = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*:|#")
+ ATTRIBUTES = re.compile(r"\s*\{([^}]*)\}\s*$")
+ HTML_ID = re.compile(r"""\bid\s*=\s*["']([^"']+)["']""")
 @@ contrib/book.py: def relink(text, base, blob, chapters):
      return rewrite(text, sub)
  
@@ contrib/book.py: def relink(text, base, blob, chapters):
 -        url = m["url"]
 -        if not m["tick"] and (url.startswith("#") or not SCHEME.match(url)):
 -            path, _, frag = url.partition("#")
--            found.add((resolve(base, path) if path else None, frag))
+-            found.add((resolve(base, decode(path)) if path else None, frag))
 +        if m["tick"]:
 +            return m[0]
 +        urls = [m["url"]]
@@ contrib/book.py: def relink(text, base, blob, chapters):
 +        for url in urls:
 +            if url.startswith("#") or not SCHEME.match(url):
 +                path, _, frag = url.partition("#")
-+                found.add((resolve(base, path) if path else None, frag))
++                found.add((resolve(base, decode(path)) if path else None, frag))
          return m[0]
  
 -    rewrite(text, sub)
@@ contrib/book.py: def anchors(text):
 -def rewrite(text, sub):
 -    """`text` with `sub` applied to each match of LINK outside code."""
 +def rewrite(text, sub, link=LINK):
-+    """`text` with `sub` applied to each match of `link` outside code."""
++    """`text` with `sub` applied to each match of `link` outside the blocks
++    in which mdbook does not read it."""
      lines = text.splitlines(keepends=True)
      blocks = {
          t.map[0]: t
-@@ contrib/book.py: def rewrite(text, sub):
+         for t in MARKDOWN.parse(text)
+-        if t.type in ("fence", "code_block", "html_block", "inline") and t.map
++        if t.type in (*VERBATIM[link], "inline") and t.map
+     }
+     out, i = [], 0
+     while i < len(lines):
          block = blocks.get(i)
          end = max(block.map[1], i + 1) if block else i + 1
          chunk = "".join(lines[i:end])

Comment thread contrib/book.py Outdated
@github-actions

Copy link
Copy Markdown
range-diff for force push 5ceb07095a → ab5ea383e1 October 16, 2026 15:40 UTC: 5 dropped, 1 added

reproduce: git range-diff e0bd68f..5ceb070 d375dd8..ab5ea38

1:  78f7ab11a4 < -:  ---------- Generate SUMMARY.md from an OKF index.md
2:  2652d48358 < -:  ---------- Check the status and stale_after of chapters
3:  ed2c0a76d2 < -:  ---------- Check links to missing files
4:  369fafa1b4 < -:  ---------- Check the anchors of links
5:  5ceb07095a < -:  ---------- Check the paths in HTML
-:  ---------- > 1:  ab5ea383e1 Check the links of the built site

@github-actions

Copy link
Copy Markdown
range-diff for force push 5ceb07095a → ab5ea383e1 October 16, 2026 15:40 UTC: 5 dropped, 1 added

reproduce: git range-diff e0bd68f..5ceb070 d375dd8..ab5ea38

1:  78f7ab11a4 < -:  ---------- Generate SUMMARY.md from an OKF index.md
2:  2652d48358 < -:  ---------- Check the status and stale_after of chapters
3:  ed2c0a76d2 < -:  ---------- Check links to missing files
4:  369fafa1b4 < -:  ---------- Check the anchors of links
5:  5ceb07095a < -:  ---------- Check the paths in HTML
-:  ---------- > 1:  ab5ea383e1 Check the links of the built site

@github-actions

Copy link
Copy Markdown
range-diff for force push ab5ea383e1 → b68e8aa7ff October 16, 2026 15:46 UTC: 1 modified

reproduce: git range-diff d375dd8..ab5ea38 d375dd8..b68e8aa

1:  ab5ea383e1 ! 1:  b68e8aa7ff Check the links of the built site
1: ab5ea383e1 ! 1: b68e8aa7ff Check the links of the built site
@@ nix/site.nix
                pytest -p no:cacheprovider
                touch $out
              '';
-+        links = pkgs.runCommand "links" { nativeBuildInputs = [ pkgs.lychee ]; } ''
++        links = pkgs.runCommand "links" { nativeBuildInputs = [ pkgs.lychee pkgs.cacert ]; } ''
 +          # Links to the repository are checked against the tree the site is built from.
 +          lychee --offline --include-fragments --no-progress --root-dir ${site} \
 +            --remap '^${repository}/blob/${rev}/ file://${../.}/' ${site}

@bc1cindy

Copy link
Copy Markdown
Member Author

I'll open a ticket per stub when #59 merges

#60, #61, #62, #63

@github-actions

Copy link
Copy Markdown
range-diff for force push b68e8aa7ff → 16a87570db October 17, 2026 01:48 UTC: 1 unchanged, 2 dropped

reproduce: git range-diff 32edbfe..b68e8aa 0f75284..16a8757

1:  44f91d0d6d < -:  ---------- Generate SUMMARY.md from an OKF index.md
2:  d375dd8cbb < -:  ---------- Check the status and stale_after of chapters
3:  b68e8aa7ff = 1:  16a87570db Check the links of the built site

@github-actions

Copy link
Copy Markdown
range-diff for force push b68e8aa7ff → 16a87570db October 17, 2026 01:48 UTC: 1 unchanged, 2 dropped

reproduce: git range-diff 32edbfe..b68e8aa 0f75284..16a8757

1:  44f91d0d6d < -:  ---------- Generate SUMMARY.md from an OKF index.md
2:  d375dd8cbb < -:  ---------- Check the status and stale_after of chapters
3:  b68e8aa7ff = 1:  16a87570db Check the links of the built site

@github-actions

Copy link
Copy Markdown
range-diff for force push 16a87570db → ef52cf7b12 October 17, 2026 14:39 UTC: 1 modified, 2 dropped

reproduce: git range-diff 32edbfe..16a8757 469fe58..ef52cf7

1:  857ded7580 < -:  ---------- Generate SUMMARY.md from an OKF index.md
2:  0f752849e8 < -:  ---------- Check the status and stale_after of chapters
3:  16a87570db ! 1:  ef52cf7b12 Check the links of the built site
3: 16a87570db ! 1: ef52cf7b12 Check the links of the built site
@@ Metadata
  ## Commit message ##
     Check the links of the built site
 
-    lychee checks the HTML mdbook writes, so a link to a missing file or
-    anchor, or a path that leaves the book, fails the build. Links to the
-    repository are checked against the tree the site is built from, as the
-    build sandbox has no network.
+    lychee checks the HTML offline; links to the repository are checked
+    against the tree the site is built from.
 
  ## nix/site.nix ##
 @@
@@ nix/site.nix
 +        '';
        };
  
-       # The Python packages contrib/book.py imports.
+       packages = {

@github-actions

Copy link
Copy Markdown
range-diff for force push ef52cf7b12 → 89f1d28f93 October 17, 2026 15:23 UTC: 1 unchanged, 2 dropped

reproduce: git range-diff 32edbfe..ef52cf7 f1fd2bd..89f1d28

1:  c2a1cbf103 < -:  ---------- Generate SUMMARY.md from an OKF index.md
2:  469fe5897a < -:  ---------- Check the status and stale_after of chapters
3:  ef52cf7b12 = 1:  89f1d28f93 Check the links of the built site

@github-actions

Copy link
Copy Markdown
range-diff for force push 3d4790830a → acedb022e8 October 18, 2026 00:27 UTC: 1 unchanged, 2 dropped

reproduce: git range-diff 32edbfe..3d47908 85a9343..acedb02

1:  b751a00653 < -:  ---------- Generate SUMMARY.md from an OKF index.md
2:  5453876f97 < -:  ---------- Check the status and stale_after of chapters
3:  3d4790830a = 1:  acedb022e8 Check the links of the built site

@bc1cindy

Copy link
Copy Markdown
Member Author

i like how minimal this turned out to be

per my comment, i think this is good to go and it seems like, although i'm not yet convinced, that it wouldn't need a followup to do .md checking as well to ensure the github previews are working

only nit is maybe cacert can be removed for --offline idk

thanks

lychee loads certs even with --offline and fails on linux without them. added a comment

the site keeps the docs/ paths, so if a link in a chapter works there it works on github too, anchors included. links starting with / were the exception, and the check now catches them

@github-actions

Copy link
Copy Markdown
range-diff for force push acedb022e8 → 357ed82e87 October 18, 2026 02:27 UTC: 1 unchanged, 2 dropped

reproduce: git range-diff 32edbfe..acedb02 b3b00d9..357ed82

1:  3049f58d46 < -:  ---------- Generate SUMMARY.md from an OKF index.md
2:  85a9343edf < -:  ---------- Check the status and stale_after of chapters
3:  acedb022e8 = 1:  357ed82e87 Check the links of the built site

1 similar comment
@github-actions

Copy link
Copy Markdown
range-diff for force push acedb022e8 → 357ed82e87 October 18, 2026 02:27 UTC: 1 unchanged, 2 dropped

reproduce: git range-diff 32edbfe..acedb02 b3b00d9..357ed82

1:  3049f58d46 < -:  ---------- Generate SUMMARY.md from an OKF index.md
2:  85a9343edf < -:  ---------- Check the status and stale_after of chapters
3:  acedb022e8 = 1:  357ed82e87 Check the links of the built site

@github-actions

Copy link
Copy Markdown
range-diff for force push 357ed82e87 → 3184a4682b October 18, 2026 03:40 UTC: 1 unchanged, 2 dropped

reproduce: git range-diff 32edbfe..357ed82 d7c812f..3184a46

1:  89cb001f0f < -:  ---------- Generate SUMMARY.md from an OKF index.md
2:  b3b00d97c9 < -:  ---------- Check the status and stale_after of chapters
3:  357ed82e87 = 1:  3184a4682b Check the links of the built site

1 similar comment
@github-actions

Copy link
Copy Markdown
range-diff for force push 357ed82e87 → 3184a4682b October 18, 2026 03:40 UTC: 1 unchanged, 2 dropped

reproduce: git range-diff 32edbfe..357ed82 d7c812f..3184a46

1:  89cb001f0f < -:  ---------- Generate SUMMARY.md from an OKF index.md
2:  b3b00d97c9 < -:  ---------- Check the status and stale_after of chapters
3:  357ed82e87 = 1:  3184a4682b Check the links of the built site

@github-actions

Copy link
Copy Markdown
range-diff for force push 3184a4682b → cdf5c105ff October 18, 2026 05:07 UTC: 1 unchanged, 2 dropped

reproduce: git range-diff 32edbfe..3184a46 61987d7..cdf5c10

1:  0fbd831f4e < -:  ---------- Generate SUMMARY.md from an OKF index.md
2:  d7c812fb02 < -:  ---------- Check the status and stale_after of chapters
3:  3184a4682b = 1:  cdf5c105ff Check the links of the built site

1 similar comment
@github-actions

Copy link
Copy Markdown
range-diff for force push 3184a4682b → cdf5c105ff October 18, 2026 05:07 UTC: 1 unchanged, 2 dropped

reproduce: git range-diff 32edbfe..3184a46 61987d7..cdf5c10

1:  0fbd831f4e < -:  ---------- Generate SUMMARY.md from an OKF index.md
2:  d7c812fb02 < -:  ---------- Check the status and stale_after of chapters
3:  3184a4682b = 1:  cdf5c105ff Check the links of the built site

Comment thread nix/site.nix
@oakagent

Copy link
Copy Markdown

ACK cc2549c (automated): reviewed 2 commits cdf5c10..cc2549c; no findings.

Base automatically changed from okf-index to main October 19, 2026 19:42
@github-actions

Copy link
Copy Markdown
range-diff for force push cc2549c2f3 → 635c89ff8a October 19, 2026 19:43 UTC: 2 unchanged

reproduce: git range-diff 61987d7..cc2549c 344b072..635c89f

1:  cdf5c105ff = 1:  24be053ee1 Check the links of the built site
2:  cc2549c2f3 = 2:  635c89ff8a Serve the not-found page's assets from /docs/

@yuval-block yuval-block left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reACK i think this just needs rebase

lychee checks the HTML offline, so a link to a page or anchor the book
does not hold fails the build. The site is checked under /docs/, where
it is served, so a link from the root, which mdbook leaves as written,
fails too.
GitHub Pages serves the site from /docs/, but mdbook writes 404.html
with <base href="/">, its default site-url, so the page shown for a
missing path loads its stylesheets, scripts and links from the root of
the domain, where none of them exist. lychee resolves relative links
against the file, not its <base>, so the link check asserts the page's
<base> itself.
@github-actions

Copy link
Copy Markdown
range-diff for force push 635c89ff8a → 9223409868 October 19, 2026 22:18 UTC: 2 unchanged

reproduce: git range-diff 344b072..635c89f b3e699e..9223409

1:  24be053ee1 = 1:  b5c425f393 Check the links of the built site
2:  635c89ff8a = 2:  9223409868 Serve the not-found page's assets from /docs/

Comment thread nix/site.nix
links = pkgs.runCommand "links" { nativeBuildInputs = [ pkgs.lychee pkgs.cacert ]; } ''
# The site is served from /docs/, so a link from the root leaves it.
mkdir root && ln -s ${site} root/docs
# lychee resolves links against the file, not its <base>.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this comment is not very clear

@bc1cindy bc1cindy added documentation Improvements or additions to documentation medium and removed medium documentation Improvements or additions to documentation labels Oct 20, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ensure all mdbook links resolve

4 participants