feat: serve redirect stubs for renamed and dissolved category slugs

Renamed or dissolved category slugs (e.g. /categories/web-servers/rpc/, /categories/code-analysis/code-linters/) returned 404, Search Console lists 7 of them, and each audit re-home was dropping the old URL's ranking.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Vinta Chen
2026-09-26 18:45:53 +08:00
co-authored by Claude
parent 3f5e5d4d27
commit c10264977e
5 changed files with 84 additions and 0 deletions
+1
View File
@@ -13,6 +13,7 @@ __pycache__/
website/output/
website/data/*
!website/data/pypi_name_overrides.json
!website/data/redirects.json
# agents
.playwright-cli/
+12
View File
@@ -772,6 +772,18 @@ def build(repo_root: Path) -> None:
parent_category=cat_by_slug[cat_slug],
)
redirects_file = website / "data" / "redirects.json"
redirects = json.loads(redirects_file.read_text(encoding="utf-8")) if redirects_file.exists() else {}
for old_path, new_path in redirects.items():
tpl_redirect = env.get_template("redirect.html")
stub = site_dir / old_path.strip("/") / "index.html"
if stub.exists():
raise ValueError(f"redirect source {old_path} is a live page; remove it from redirects.json")
if not (site_dir / new_path.strip("/") / "index.html").exists():
raise ValueError(f"redirect target {new_path} for {old_path} does not exist")
stub.parent.mkdir(parents=True, exist_ok=True)
stub.write_text(tpl_redirect.render(target_url=SITE_URL + new_path.lstrip("/")), encoding="utf-8")
static_src = website / "static"
static_dst = site_dir / "static"
if static_src.exists():
+10
View File
@@ -0,0 +1,10 @@
{
"/categories/": "/",
"/categories/ai-and-agents/pre-trained-models-and-inference/": "/categories/ai-and-agents/pre-trained-models/",
"/categories/cli-tools/productivity-tools/": "/categories/cli-tools/",
"/categories/code-analysis/code-linters/": "/categories/code-analysis/linters-and-formatters/",
"/categories/data-analysis/financial-data/": "/categories/data-ingestion-etl/financial-data/",
"/categories/distributed-computing/batch-processing/": "/categories/distributed-computing/",
"/categories/gui-development/terminal/": "/categories/cli-development/tui-frameworks/",
"/categories/web-servers/rpc/": "/categories/web-apis/rpc/"
}
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<meta charset="utf-8">
<title>Redirecting&hellip;</title>
<link rel="canonical" href="{{ target_url }}">
<script>location="{{ target_url }}"</script>
<meta http-equiv="refresh" content="0; url={{ target_url }}">
<meta name="robots" content="noindex">
<h1>Redirecting&hellip;</h1>
<a href="{{ target_url }}">Click here if you are not redirected.</a>
</html>
+50
View File
@@ -955,6 +955,56 @@ class TestBuild:
assert 'data-url="/categories/ai-ml/"' in index_html
assert 'data-url="/categories/deep-learning/vision/"' in index_html
_REDIRECT_README = textwrap.dedent("""\
# Awesome Python
Intro.
## Projects
**Tools**
### Widgets
- [w1](https://example.com/w1) - A widget.
## Contributing
Help!
""")
def _write_redirects(self, tmp_path, redirects):
data_dir = tmp_path / "website" / "data"
data_dir.mkdir(parents=True)
(data_dir / "redirects.json").write_text(json.dumps(redirects), encoding="utf-8")
def test_build_writes_redirect_stub_outside_sitemap(self, tmp_path):
self._copy_real_templates(tmp_path)
(tmp_path / "README.md").write_text(self._REDIRECT_README, encoding="utf-8")
self._write_redirects(tmp_path, {"/categories/old-widgets/": "/categories/widgets/"})
build(tmp_path)
site = tmp_path / "website" / "output"
stub = (site / "categories" / "old-widgets" / "index.html").read_text(encoding="utf-8")
assert '<link rel="canonical" href="https://awesome-python.com/categories/widgets/">' in stub
assert '<meta http-equiv="refresh" content="0; url=https://awesome-python.com/categories/widgets/">' in stub
assert '<meta name="robots" content="noindex">' in stub
assert "old-widgets" not in (site / "sitemap.xml").read_text(encoding="utf-8")
def test_build_rejects_redirect_to_missing_page(self, tmp_path):
self._copy_real_templates(tmp_path)
(tmp_path / "README.md").write_text(self._REDIRECT_README, encoding="utf-8")
self._write_redirects(tmp_path, {"/categories/old-widgets/": "/categories/gone/"})
with pytest.raises(ValueError, match="does not exist"):
build(tmp_path)
def test_build_rejects_redirect_over_live_page(self, tmp_path):
self._copy_real_templates(tmp_path)
(tmp_path / "README.md").write_text(self._REDIRECT_README, encoding="utf-8")
self._write_redirects(tmp_path, {"/categories/widgets/": "/"})
with pytest.raises(ValueError, match="is a live page"):
build(tmp_path)
# ---------------------------------------------------------------------------
# extract_github_repo