Files
mcg-website/tools/build_data.py
T
luxick bef5182545 Migrate WordPress site to Hugo
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>
2026-07-28 19:20:11 +02:00

170 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Generate data/*.yaml.
Two sources:
* ``data/termine.yaml`` comes from the six TablePress tables in the WXR export.
* ``data/members.yaml`` and ``data/memoriam.yaml`` come from the *live* homepage.
Those two blocks were WordPress widgets, and WXR does not export widgets at all,
so scraping the rendered page is the only way to recover them.
Run once during the migration. After that the YAML files are the source of truth.
"""
from __future__ import annotations
import html
import json
import re
import sys
import urllib.request
from pathlib import Path
from wxr import REPO_ROOT, clean_media_name, load_items
LIVE_HOME = "https://motorradclub-giebelwald.de/"
USER_AGENT = "mcg-website-migration/1.0 (+https://mcg.luxick.de)"
DATA = REPO_ROOT / "data"
def yaml_str(value: str) -> str:
"""Always double-quote; the cells contain colons, umlauts and inline HTML."""
return '"' + value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") + '"'
# --------------------------------------------------------------------------- termine
def build_termine() -> None:
items = load_items()
tables = [i for i in items.values() if i.post_type == "tablepress_table"]
years: dict[int, list[dict[str, str]]] = {}
for table in tables:
match = re.search(r"(\d{4})", table.title)
if not match:
print(f"!! skipping table without a year in the title: {table.title}",
file=sys.stderr)
continue
year = int(match.group(1))
rows = []
for row in json.loads(table.content):
date = (row[0] if len(row) > 0 else "").strip()
desc = (row[1] if len(row) > 1 else "").strip()
if not date and not desc:
continue # TablePress spacer rows
rows.append({"date": date, "description": desc})
years[year] = rows
lines = [
"# Vereinstermine, ursprünglich TablePress-Tabellen aus WordPress.",
"# Neue Termine: einfach unter dem passenden Jahr ergänzen.",
"",
]
for year in sorted(years, reverse=True):
lines.append(f'"{year}":')
if not years[year]:
lines[-1] += " []"
continue
for row in years[year]:
lines.append(f" - date: {yaml_str(row['date'])}")
lines.append(f" description: {yaml_str(row['description'])}")
lines.append("")
(DATA / "termine.yaml").write_text("\n".join(lines).rstrip() + "\n",
encoding="utf-8")
total = sum(len(v) for v in years.values())
print(f"termine.yaml: {len(years)} Jahre, {total} Termine")
# ------------------------------------------------------------------- widget scraping
def live_home() -> str:
req = urllib.request.Request(LIVE_HOME, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(req, timeout=60) as resp:
return resp.read().decode("utf-8", errors="replace")
GALLERY_ITEM = re.compile(
r"<figure class='gallery-item'>.*?"
r"<a href='(?P<href>[^']+)'>.*?"
r"(?:<figcaption[^>]*>(?P<caption>.*?)</figcaption>)?"
r"</figure>",
re.S,
)
def gallery_entries(fragment: str) -> list[tuple[str, str]]:
out = []
for m in GALLERY_ITEM.finditer(fragment):
href = m.group("href")
caption = html.unescape(re.sub(r"<[^>]+>", "", m.group("caption") or "")).strip()
out.append((href, caption))
return out
def image_ref(url: str) -> str:
"""Map an uploads URL to the committed path under assets/img/."""
return "img/" + clean_media_name(url.rsplit("/", 1)[-1])
def build_widgets() -> None:
page = live_home()
# Each widget renders as <h4 class="widget-title">NAME</h4> followed by a WP
# gallery. Keep the trailing </figure> inside the group, otherwise the last
# entry of every gallery is silently dropped.
members_html = re.search(
r"Unsere Mitglieder</h4>(.*?</figure>)\s*</div>", page, re.S
)
memoriam_html = re.search(
r"In Erinnerung</h4>(.*?</figure>)\s*</div>", page, re.S
)
if not members_html or not memoriam_html:
raise SystemExit("could not locate the member/memoriam widgets on the live page")
members = gallery_entries(members_html.group(1))
memoriam = gallery_entries(memoriam_html.group(1))
# The WordPress gallery widget shuffles its order on every request, so the
# source order carries no meaning. Sort by name for a stable, readable list.
members.sort(key=lambda entry: entry[1].casefold())
lines = [
"# Mitglieder-Galerie der Startseite.",
"# Aus dem WordPress-Widget übernommen (Widgets sind nicht im XML-Export enthalten).",
"",
]
for url, name in members:
lines.append(f"- name: {yaml_str(name)}")
lines.append(f" image: {yaml_str(image_ref(url))}")
(DATA / "members.yaml").write_text("\n".join(lines) + "\n", encoding="utf-8")
print(f"members.yaml: {len(members)} Mitglieder")
lines = [
"# \"In Erinnerung\" ebenfalls aus einem WordPress-Widget übernommen.",
"",
]
for url, caption in memoriam:
# Captions look like "Roland Groos ✝ 27.5.2020".
name, _, died = caption.partition("")
lines.append(f"- name: {yaml_str(name.strip())}")
lines.append(f" died: {yaml_str(died.strip())}")
lines.append(f" image: {yaml_str(image_ref(url))}")
(DATA / "memoriam.yaml").write_text("\n".join(lines) + "\n", encoding="utf-8")
print(f"memoriam.yaml: {len(memoriam)} Einträge")
return [image_ref(u) for u, _ in members] + [image_ref(u) for u, _ in memoriam]
def main() -> int:
DATA.mkdir(parents=True, exist_ok=True)
build_termine()
build_widgets()
return 0
if __name__ == "__main__":
raise SystemExit(main())