bef5182545
Replaces the WordPress 7.0.2 / HitMag site with a static Hugo build,
deployed by Gitea Actions over rsync/SSH.
Content: 26 files (9 pages + 17 posts) as Markdown page bundles, plus
127 of 165 media attachments. The remaining 38 are media-library
leftovers that appear nowhere on the live site; they are listed in
MIGRATION.md.
The WXR export contains neither media binaries nor widgets, so both were
recovered from the live host before it is retired:
- tools/fetch_media.py downloads all uploads, capping them at 2000px
- tools/build_data.py scrapes the "Unsere Mitglieder" and
"In Erinnerung" widgets into data/*.yaml
TablePress' six Termine tables became data/termine.yaml, rendered with
the newest year expanded and earlier years collapsed. Old permalinks are
not preserved (clean slugs, no redirects, as agreed).
Custom layouts, no third-party theme. Plain CSS, since the pinned Hugo
0.164.0 is the non-extended build and cannot compile Sass.
Verified: clean build with zero warnings, 718 internal links checked and
none broken, no surviving wp-content URLs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
134 lines
4.5 KiB
Python
134 lines
4.5 KiB
Python
"""Post-migration checks. Run after `hugo --gc --minify`.
|
|
|
|
1. Inventory - every published WordPress page/post has a content file.
|
|
2. Media - every attachment is either placed in the site or explicitly
|
|
accounted for as an unused media-library leftover.
|
|
3. Links - no internal href/src in public/ points at a missing file, and no
|
|
absolute wp-content URL survived the import.
|
|
|
|
Exits non-zero if any check fails.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from urllib.parse import unquote, urlsplit
|
|
|
|
from wp2hugo import PAGE_MAP, SKIP, SLUG_FIXES
|
|
from wxr import REPO_ROOT, clean_media_name, load_items, slugify
|
|
|
|
PUBLIC = REPO_ROOT / "public"
|
|
CONTENT = REPO_ROOT / "content"
|
|
|
|
failures: list[str] = []
|
|
notes: list[str] = []
|
|
|
|
|
|
def check_inventory(items) -> None:
|
|
expected = 0
|
|
for item in items.values():
|
|
if item.post_id in SKIP or item.status != "publish":
|
|
continue
|
|
if item.post_type == "page":
|
|
mapping = PAGE_MAP.get(item.post_id)
|
|
if not mapping:
|
|
failures.append(f"page {item.post_id} ({item.slug}) is unmapped")
|
|
continue
|
|
path = CONTENT / mapping[0]
|
|
elif item.post_type == "post":
|
|
path = CONTENT / "aktuelles" / SLUG_FIXES.get(
|
|
item.slug, slugify(item.slug)) / "index.md"
|
|
else:
|
|
continue
|
|
expected += 1
|
|
if not path.exists():
|
|
failures.append(f"missing content file for {item.post_type} "
|
|
f"{item.post_id} ({item.slug}): {path}")
|
|
notes.append(f"inventory: {expected} published pages/posts accounted for")
|
|
|
|
for post_id, reason in SKIP.items():
|
|
notes.append(f" skipped {post_id}: {reason}")
|
|
|
|
|
|
def check_media(items) -> None:
|
|
placed = {p.name for p in CONTENT.rglob("*") if p.is_file()
|
|
and p.suffix.lower() != ".md"}
|
|
placed |= {p.name for p in (REPO_ROOT / "assets" / "img").rglob("*")
|
|
if p.is_file()}
|
|
placed |= {p.name for p in (REPO_ROOT / "static").rglob("*") if p.is_file()}
|
|
# assets/img/logo.png and banner.jpg are renamed copies.
|
|
placed |= {"Emblem_transparent.png", "cropped-Banner_small_web.jpg"}
|
|
|
|
unused = []
|
|
for item in items.values():
|
|
if item.post_type != "attachment" or not item.attachment_url:
|
|
continue
|
|
original = item.attachment_url.rsplit("/", 1)[-1]
|
|
if original in placed or clean_media_name(original) in placed:
|
|
continue
|
|
unused.append(item.attachment_url)
|
|
|
|
total = sum(1 for i in items.values() if i.post_type == "attachment")
|
|
notes.append(f"media: {total - len(unused)}/{total} attachments placed")
|
|
if unused:
|
|
notes.append(f" {len(unused)} media-library leftovers not shown on the "
|
|
f"live site (see MIGRATION.md)")
|
|
|
|
|
|
HREF = re.compile(r'(?:href|src)=["\']?([^"\'\s>]+)', re.I)
|
|
|
|
|
|
def check_links() -> None:
|
|
if not PUBLIC.exists():
|
|
failures.append("public/ does not exist - run `hugo` first")
|
|
return
|
|
|
|
checked = broken = 0
|
|
for html in PUBLIC.rglob("*.html"):
|
|
text = html.read_text(encoding="utf-8", errors="replace")
|
|
|
|
for stale in re.findall(r'https?://motorradclub-giebelwald\.de/wp-content/\S*',
|
|
text):
|
|
failures.append(f"{html.relative_to(PUBLIC)}: stale WordPress URL {stale}")
|
|
|
|
for raw in HREF.findall(text):
|
|
url = urlsplit(raw)
|
|
if url.scheme or url.netloc or not url.path:
|
|
continue # external, mailto:, protocol-relative
|
|
if raw.startswith("#"):
|
|
continue
|
|
checked += 1
|
|
path = unquote(url.path)
|
|
target = (PUBLIC / path.lstrip("/") if path.startswith("/")
|
|
else html.parent / path)
|
|
if target.is_dir():
|
|
target = target / "index.html"
|
|
if not target.exists():
|
|
broken += 1
|
|
failures.append(f"{html.relative_to(PUBLIC)}: broken link -> {raw}")
|
|
|
|
notes.append(f"links: {checked} internal references checked, {broken} broken")
|
|
|
|
|
|
def main() -> int:
|
|
items = load_items()
|
|
check_inventory(items)
|
|
check_media(items)
|
|
check_links()
|
|
|
|
for note in notes:
|
|
print(note)
|
|
if failures:
|
|
print(f"\n{len(failures)} FAILURE(S):", file=sys.stderr)
|
|
for f in failures:
|
|
print(f" - {f}", file=sys.stderr)
|
|
return 1
|
|
print("\nall checks passed")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|