chore: save iqon static deploy snapshot
This commit is contained in:
Executable
+123
@@ -0,0 +1,123 @@
|
||||
#!/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())
|
||||
Executable
+110
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPEC: wp-snippets/README.md#verification
|
||||
# Compliance date: 2026-05-20
|
||||
# Launch type: manual post-deployment verification
|
||||
# Registry: iqon Phase 1 hardening verification
|
||||
|
||||
set -u
|
||||
|
||||
BASE_URL="${1:-https://iqon.com}"
|
||||
FAILED=0
|
||||
|
||||
note() {
|
||||
printf '\n[%s] %s\n' "$1" "$2"
|
||||
}
|
||||
|
||||
fail() {
|
||||
FAILED=1
|
||||
printf '[FAIL] %s\n' "$1"
|
||||
}
|
||||
|
||||
pass() {
|
||||
printf '[PASS] %s\n' "$1"
|
||||
}
|
||||
|
||||
fetch_headers() {
|
||||
curl -sS -I -L "$BASE_URL/"
|
||||
}
|
||||
|
||||
fetch_home() {
|
||||
curl -sS -L "$BASE_URL/"
|
||||
}
|
||||
|
||||
note CHECK "Security headers"
|
||||
if ! HEADERS="$(fetch_headers)"; then
|
||||
HEADERS=""
|
||||
fail 'could not fetch homepage headers'
|
||||
fi
|
||||
printf '%s\n' "$HEADERS" | rg -qi '^x-content-type-options:\s*nosniff' \
|
||||
&& pass 'X-Content-Type-Options present' \
|
||||
|| fail 'X-Content-Type-Options missing'
|
||||
printf '%s\n' "$HEADERS" | rg -qi '^x-frame-options:\s*SAMEORIGIN' \
|
||||
&& pass 'X-Frame-Options present' \
|
||||
|| fail 'X-Frame-Options missing'
|
||||
printf '%s\n' "$HEADERS" | rg -qi '^referrer-policy:' \
|
||||
&& pass 'Referrer-Policy present' \
|
||||
|| fail 'Referrer-Policy missing'
|
||||
printf '%s\n' "$HEADERS" | rg -qi '^permissions-policy:' \
|
||||
&& pass 'Permissions-Policy present' \
|
||||
|| fail 'Permissions-Policy missing'
|
||||
printf '%s\n' "$HEADERS" | rg -qi '^content-security-policy:' \
|
||||
&& pass 'Content-Security-Policy present' \
|
||||
|| fail 'Content-Security-Policy missing'
|
||||
|
||||
note CHECK "Homepage HTML"
|
||||
if HOME="$(fetch_home)"; then
|
||||
printf '%s\n' "$HOME" | rg -q 'name=["'\'']viewport["'\'']' \
|
||||
&& pass 'viewport present' \
|
||||
|| fail 'viewport missing'
|
||||
printf '%s\n' "$HOME" | rg -q 'iqon-skip-link' \
|
||||
&& pass 'skip link present' \
|
||||
|| fail 'skip link missing'
|
||||
printf '%s\n' "$HOME" | rg -q 'http://www\.iqon\.com|http://iqon\.com|http://maps\.google\.com|http://maps\.googleapis\.com' \
|
||||
&& fail 'known HTTP URLs still present' \
|
||||
|| pass 'known HTTP URLs absent'
|
||||
printf '%s\n' "$HOME" | rg -q 'property=["'\'']og:site_name["'\''][^>]*content=["'\'']- IQON["'\'']' \
|
||||
&& fail 'og:site_name still has - IQON' \
|
||||
|| pass 'og:site_name no longer has - IQON'
|
||||
else
|
||||
fail 'could not fetch homepage HTML'
|
||||
fi
|
||||
|
||||
note CHECK "REST users endpoint"
|
||||
if USERS_BODY="$(curl -sS -L "$BASE_URL/wp-json/wp/v2/users")"; then
|
||||
printf '%s\n' "$USERS_BODY" | rg -q '"slug"\s*:\s*"(admin|iqnadm01)"' \
|
||||
&& fail 'public REST users still exposed' \
|
||||
|| pass 'public REST users not exposed'
|
||||
else
|
||||
fail 'could not fetch REST users endpoint'
|
||||
fi
|
||||
|
||||
note CHECK "Install documents"
|
||||
README_STATUS="$(curl -sS -o /dev/null -w '%{http_code}' -L "$BASE_URL/readme.html")"
|
||||
case "$README_STATUS" in
|
||||
403|404) pass "readme.html blocked with $README_STATUS" ;;
|
||||
*) fail "readme.html returned $README_STATUS" ;;
|
||||
esac
|
||||
|
||||
note CHECK "XML-RPC HEAD smoke"
|
||||
XMLRPC_STATUS="$(curl -sS -o /dev/null -w '%{http_code}' -I --http1.1 "$BASE_URL/xmlrpc.php")"
|
||||
XMLRPC_CURL_STATUS=$?
|
||||
case "$XMLRPC_STATUS" in
|
||||
403|404|405) pass "xmlrpc.php not plainly usable via HEAD: $XMLRPC_STATUS" ;;
|
||||
000)
|
||||
if [ "$XMLRPC_CURL_STATUS" -eq 52 ]; then
|
||||
pass "xmlrpc.php returned empty reply via HEAD: $XMLRPC_STATUS"
|
||||
else
|
||||
fail "xmlrpc.php check failed with curl status $XMLRPC_CURL_STATUS"
|
||||
fi
|
||||
;;
|
||||
*) fail "xmlrpc.php returned $XMLRPC_STATUS; run safe POST check" ;;
|
||||
esac
|
||||
|
||||
printf '\n'
|
||||
if [ "$FAILED" -eq 0 ]; then
|
||||
printf '[OK] iqon Phase 1 verification passed for %s\n' "$BASE_URL"
|
||||
else
|
||||
printf '[FAIL] iqon Phase 1 verification failed for %s\n' "$BASE_URL"
|
||||
fi
|
||||
|
||||
exit "$FAILED"
|
||||
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create a CSV inventory from the iqon.com page sitemap.
|
||||
|
||||
SPEC: reports/live-captures/2026-05-20/page-sitemap.xml
|
||||
Compliance date: 2026-05-20
|
||||
Launch type: manual SEO inventory generation
|
||||
Registry: iqon sitemap inventory parser
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import sys
|
||||
import urllib.parse
|
||||
import xml.etree.ElementTree as ET
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SITEMAP_NS = {"sm": "http://www.sitemaps.org/schemas/sitemap/0.9"}
|
||||
|
||||
|
||||
def classify_url(url: str, duplicate_count: int) -> tuple[str, str]:
|
||||
parsed = urllib.parse.urlparse(url)
|
||||
decoded_path = urllib.parse.unquote(parsed.path)
|
||||
|
||||
if duplicate_count > 1:
|
||||
return "duplicate", "canonicalize_or_remove_duplicate"
|
||||
if "%" in parsed.path:
|
||||
return "encoded_multilingual", "decide_hreflang_or_noindex_redirect"
|
||||
if decoded_path in {"/homepage/", "/hows-business/"}:
|
||||
return "legacy_home_alias", "redirect_to_root_or_rewrite"
|
||||
if "retail-wholesail" in decoded_path:
|
||||
return "typo_slug", "redirect_to_correct_spelling"
|
||||
if decoded_path == "/services-solutions/":
|
||||
return "thin_landing", "rewrite_as_services_overview"
|
||||
if decoded_path == "/contact/":
|
||||
return "contact", "verify_contact_data_then_keep"
|
||||
if decoded_path == "/":
|
||||
return "homepage", "rewrite_homepage_metadata_and_content"
|
||||
|
||||
return "service_or_content", "review_rewrite_or_merge"
|
||||
|
||||
|
||||
def iter_urls(path: Path) -> list[dict[str, str]]:
|
||||
tree = ET.parse(path)
|
||||
urls: list[dict[str, str]] = []
|
||||
|
||||
for node in tree.findall("sm:url", SITEMAP_NS):
|
||||
loc_node = node.find("sm:loc", SITEMAP_NS)
|
||||
lastmod_node = node.find("sm:lastmod", SITEMAP_NS)
|
||||
if loc_node is None or not loc_node.text:
|
||||
continue
|
||||
|
||||
loc = loc_node.text.strip()
|
||||
parsed = urllib.parse.urlparse(loc)
|
||||
urls.append(
|
||||
{
|
||||
"url": loc,
|
||||
"decoded_path": urllib.parse.unquote(parsed.path),
|
||||
"lastmod": (lastmod_node.text.strip() if lastmod_node is not None and lastmod_node.text else ""),
|
||||
}
|
||||
)
|
||||
|
||||
counts = Counter(item["url"] for item in urls)
|
||||
for item in urls:
|
||||
duplicate_count = counts[item["url"]]
|
||||
item["duplicate_count"] = str(duplicate_count)
|
||||
item["class"], item["recommended_action"] = classify_url(item["url"], duplicate_count)
|
||||
|
||||
return urls
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) not in {2, 3}:
|
||||
print("Usage: iqon-sitemap-inventory.py <page-sitemap.xml> [output.csv]", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
rows = iter_urls(Path(sys.argv[1]))
|
||||
output = open(sys.argv[2], "w", newline="", encoding="utf-8") if len(sys.argv) == 3 else sys.stdout
|
||||
writer = csv.DictWriter(
|
||||
output,
|
||||
fieldnames=["url", "decoded_path", "lastmod", "duplicate_count", "class", "recommended_action"],
|
||||
)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
if output is not sys.stdout:
|
||||
output.close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user