Hugo site: layouts, config and pilot content
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDRjtmAiNpo5mK5N3d7QwZ
This commit is contained in:
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check that every internal link and image of the built site points to an existing file.
|
||||
|
||||
Usage: tools/check_links.py [public] (exit status 1 when something is broken)
|
||||
"""
|
||||
import html
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote, urldefrag, urlparse
|
||||
|
||||
# minified pages drop the quotes around simple attribute values
|
||||
ATTR = re.compile(r'\b(?:href|src)=(?:"([^"]*)"|\'([^\']*)\'|([^\s>"\']+))', re.I)
|
||||
|
||||
|
||||
def main():
|
||||
root = Path(sys.argv[1] if len(sys.argv) > 1 else "public").resolve()
|
||||
broken, checked = {}, 0
|
||||
for page in sorted(root.rglob("*.html")):
|
||||
text = page.read_text(encoding="utf-8", errors="replace")
|
||||
for url in {a or b or c for a, b, c in ATTR.findall(text)}:
|
||||
url = urldefrag(html.unescape(url))[0]
|
||||
parsed = urlparse(url)
|
||||
if not url or parsed.scheme or parsed.netloc:
|
||||
continue # external link, mailto:, pure anchor
|
||||
path = unquote(parsed.path)
|
||||
target = (root / path.lstrip("/")) if path.startswith("/") else (page.parent / path)
|
||||
target = target.resolve()
|
||||
if target.is_dir():
|
||||
target = target / "index.html"
|
||||
checked += 1
|
||||
if not target.is_file():
|
||||
broken.setdefault(url, []).append(str(page.relative_to(root)))
|
||||
print(f"{checked} internal references checked, {len(broken)} broken")
|
||||
for url, pages in sorted(broken.items()):
|
||||
print(f" {url} (in {pages[0]}{f' and {len(pages) - 1} more' if len(pages) > 1 else ''})")
|
||||
sys.exit(1 if broken else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user