diff --git a/website/build.py b/website/build.py
index d447c384..27b9adc0 100644
--- a/website/build.py
+++ b/website/build.py
@@ -20,6 +20,7 @@ from readme_parser import AlsoSee, ParsedGroup, ParsedSection, parse_readme, par
GITHUB_REPO_URL_RE = re.compile(r"^https?://github\.com/([^/]+/[^/]+?)(?:\.git)?/?$")
MARKDOWN_LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)\s]+)\)")
BULLET_LINE_RE = re.compile(r"^\s*-\s")
+ANCHOR_LINK_ATTRS_RE = re.compile(r'href="(#[^"]*)" target="_blank" rel="noopener"')
SITE_URL = "https://awesome-python.com/"
SITEMAP_URL = f"{SITE_URL}sitemap.xml"
SITEMAP_NS = "http://www.sitemaps.org/schemas/sitemap/0.9"
@@ -466,6 +467,21 @@ def link_llms_category_index_to_canonical_pages(markdown: str, categories: Seque
return "".join(out)
+def link_description_anchors_to_category_pages(categories: Sequence[ParsedSection]) -> None:
+ """Point README anchor links in section descriptions at category pages, which lack those anchors."""
+ category_paths = {}
+ for category in categories:
+ category_paths[f"#{category['slug']}"] = category_path(category)
+ category_paths[github_markdown_anchor(category["name"])] = category_path(category)
+
+ def replace_attrs(match: re.Match[str]) -> str:
+ path = category_paths.get(match.group(1))
+ return f'href="{path}"' if path else match.group(0)
+
+ for category in categories:
+ category["description_html"] = ANCHOR_LINK_ATTRS_RE.sub(replace_attrs, category["description_html"])
+
+
def build_llms_txt(
template_text: str,
*,
@@ -645,6 +661,7 @@ def build(repo_root: Path) -> None:
duplicates = {s for s, n in Counter(all_top_level_slugs).items() if n > 1}
if duplicates:
raise ValueError(f"slug collision in /categories/ namespace: {sorted(duplicates)}. Rename a category or group so their slugs differ.")
+ link_description_anchors_to_category_pages(categories)
total_entries = sum(c["entry_count"] for c in categories)
entries = extract_entries(categories, parsed_groups)
build_date = datetime.now(UTC)
diff --git a/website/tests/test_build.py b/website/tests/test_build.py
index 78495fa8..3588a719 100644
--- a/website/tests/test_build.py
+++ b/website/tests/test_build.py
@@ -295,6 +295,41 @@ class TestBuild:
assert "42" in category_html
assert "2026-01-01T00:00:00+00:00" in category_html
+ def test_build_links_description_anchors_to_category_pages(self, tmp_path):
+ readme = textwrap.dedent("""\
+ # Awesome Python
+
+ Intro.
+
+ ## Projects
+
+ **Tools**
+
+ ## Audio & Video
+
+ _Media tools._
+
+ - [a1](https://example.com/a1) - A media tool.
+
+ ## Widgets
+
+ _Widget libraries. Also see [Audio & Video](#audio--video) and [Gadgets](#gadgets)._
+
+ - [w1](https://example.com/w1) - A widget.
+
+ # Contributing
+
+ Help!
+ """)
+ (tmp_path / "README.md").write_text(readme, encoding="utf-8")
+ self._copy_real_templates(tmp_path)
+
+ build(tmp_path)
+
+ category_html = (tmp_path / "website" / "output" / "categories" / "widgets" / "index.html").read_text(encoding="utf-8")
+ assert 'Also see Audio & Video' in category_html
+ assert 'Gadgets' in category_html
+
def test_build_creates_llms_text_alternate_without_sponsors(self, tmp_path):
readme = textwrap.dedent("""\
# Awesome Python