124 lines
3.7 KiB
Python
Executable File
124 lines
3.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Generate a draft redirect map from the iqon sitemap inventory.
|
|
|
|
SPEC: seo/url-cleanup-plan-2026-05-20.md#redirect-drafts
|
|
Compliance date: 2026-05-20
|
|
Launch type: manual SEO migration planning
|
|
Registry: iqon redirect map generator
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import csv
|
|
import sys
|
|
import urllib.parse
|
|
from pathlib import Path
|
|
|
|
|
|
DIRECT_PATH_REDIRECTS = {
|
|
"/homepage/": "/",
|
|
"/hows-business/": "/",
|
|
"/industry-specific-services/": "/services-solutions/industry-specific-services/",
|
|
"/services-solutions/industry-specific-services/retail-wholesail/": "/services-solutions/industry-specific-services/retail-wholesale/",
|
|
}
|
|
|
|
RU_PREFIX = "/услуги-решения/"
|
|
EN_PREFIX = "/services-solutions/"
|
|
|
|
|
|
def normalize_path(path: str) -> str:
|
|
if not path.startswith("/"):
|
|
path = "/" + path
|
|
if not path.endswith("/"):
|
|
path += "/"
|
|
return path
|
|
|
|
|
|
def target_for(decoded_path: str) -> tuple[str, str, str]:
|
|
path = normalize_path(decoded_path)
|
|
|
|
if path in DIRECT_PATH_REDIRECTS:
|
|
return DIRECT_PATH_REDIRECTS[path], "301", "direct_alias_or_typo"
|
|
|
|
if path.startswith(RU_PREFIX):
|
|
english_path = EN_PREFIX + path[len(RU_PREFIX) :]
|
|
return normalize_path(english_path), "301", "ru_to_english_default_policy"
|
|
|
|
return "", "", "manual_review"
|
|
|
|
|
|
def read_inventory(path: Path) -> list[dict[str, str]]:
|
|
with path.open(encoding="utf-8", newline="") as handle:
|
|
return list(csv.DictReader(handle))
|
|
|
|
|
|
def write_redirect_csv(rows: list[dict[str, str]], output_path: Path) -> None:
|
|
seen: set[tuple[str, str]] = set()
|
|
output_rows: list[dict[str, str]] = []
|
|
|
|
for row in rows:
|
|
source = normalize_path(row["decoded_path"])
|
|
target, status, reason = target_for(source)
|
|
if not target:
|
|
continue
|
|
|
|
key = (source, target)
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
|
|
output_rows.append(
|
|
{
|
|
"source_path": source,
|
|
"target_path": target,
|
|
"status": status,
|
|
"reason": reason,
|
|
"source_url": row["url"],
|
|
}
|
|
)
|
|
|
|
with output_path.open("w", encoding="utf-8", newline="") as handle:
|
|
writer = csv.DictWriter(handle, fieldnames=["source_path", "target_path", "status", "reason", "source_url"])
|
|
writer.writeheader()
|
|
writer.writerows(output_rows)
|
|
|
|
|
|
def write_nginx_map(redirect_csv: Path, output_path: Path) -> None:
|
|
rows = read_inventory_csv(redirect_csv)
|
|
|
|
with output_path.open("w", encoding="utf-8") as handle:
|
|
handle.write("# IQON redirect draft generated from seo/sitemap-inventory-2026-05-20.csv\n")
|
|
handle.write("# Review before deployment. Not applied to live hosting.\n\n")
|
|
for row in rows:
|
|
source = urllib.parse.quote(row["source_path"], safe="/-._~")
|
|
target = urllib.parse.quote(row["target_path"], safe="/-._~")
|
|
escaped_source = source.strip("/")
|
|
handle.write(f"rewrite ^/{escaped_source}/?$ {target} permanent;\n")
|
|
|
|
|
|
def read_inventory_csv(path: Path) -> list[dict[str, str]]:
|
|
with path.open(encoding="utf-8", newline="") as handle:
|
|
return list(csv.DictReader(handle))
|
|
|
|
|
|
def main() -> int:
|
|
if len(sys.argv) != 4:
|
|
print(
|
|
"Usage: iqon-generate-redirect-map.py <inventory.csv> <redirects.csv> <nginx.conf>",
|
|
file=sys.stderr,
|
|
)
|
|
return 2
|
|
|
|
inventory_path = Path(sys.argv[1])
|
|
redirect_csv = Path(sys.argv[2])
|
|
nginx_conf = Path(sys.argv[3])
|
|
|
|
rows = read_inventory(inventory_path)
|
|
write_redirect_csv(rows, redirect_csv)
|
|
write_nginx_map(redirect_csv, nginx_conf)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|