v0.5.115: add History routing and SEO prerender for Climperor Web.

Path URLs, crawlable hero/mechanics pages, and sitemap make the static site indexable while keeping SPA hydration.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
voson
2026-07-30 04:55:33 +08:00
co-authored by Cursor
parent 38f46ad2ea
commit 544ea42d40
16 changed files with 838 additions and 81 deletions
+46 -5
View File
@@ -2,12 +2,15 @@
Usage:
python export_relations_site.py [--out dist/relations] [--with-videos]
[--ability-video-base URL] [--static-asset-base URL]
[--ability-video-base URL] [--static-asset-base URL] [--site-origin URL]
Copies web/relations/ + a snapshot of the /api/data payload (data.json) +
Copies web/frontend/ + a snapshot of the /api/data payload (data.json) +
the referenced image assets into one directory, ready for any static host
(GitHub Pages, Cloudflare Pages, nginx, ...).
Also runs seo_prerender: crawlable HTML for heroes/mechanics/top pages,
plus sitemap.xml / llms.txt / robots.txt / _redirects (History SPA).
Notes:
- Only already-cached assets are exported. For full ability-icon coverage
run `python fetch_hero_abilities.py --icons-only` first.
@@ -50,9 +53,10 @@ from shared.paths import (
WEB_DIST,
)
from seo_prerender import DEFAULT_SITE_ORIGIN, write_seo_bundle
from serve_relations import WEB_DIR, build_payload
SITE_VERSION = "0.5.114"
SITE_VERSION = "0.5.115"
DEFAULT_OSS_BASE = "https://climperor.oss-cn-shanghai.aliyuncs.com"
@@ -133,14 +137,17 @@ def write_config_js(
ability_video_base: str,
static_asset_base: str,
site_version: str,
site_origin: str,
) -> None:
"""Write config.js consumed by app.js."""
video = (ability_video_base or "").strip().rstrip("/")
static = (static_asset_base or "").strip().rstrip("/")
ver = (site_version or "").strip()
origin = (site_origin or "").strip().rstrip("/")
(out / "config.js").write_text(
"/* generated by export_relations_site.py — do not edit */\n"
f"var SITE_VERSION = {json.dumps(ver, ensure_ascii=False)};\n"
f"var SITE_ORIGIN = {json.dumps(origin, ensure_ascii=False)};\n"
f"var ABILITY_VIDEO_BASE = {json.dumps(video, ensure_ascii=False)};\n"
f"var STATIC_ASSET_BASE = {json.dumps(static, ensure_ascii=False)};\n",
encoding="utf-8",
@@ -170,6 +177,12 @@ def main() -> None:
"when set, image dirs are not copied into dist; "
"falls back to STATIC_ASSET_BASE env, else empty",
)
ap.add_argument(
"--site-origin",
default=None,
help="canonical site origin for SEO (sitemap / og / config SITE_ORIGIN); "
"falls back to SITE_ORIGIN env, else https://dota2.refining.dev",
)
args = ap.parse_args()
video_base = (
@@ -182,6 +195,11 @@ def main() -> None:
if args.static_asset_base is not None
else os.environ.get("STATIC_ASSET_BASE", "")
)
site_origin = (
args.site_origin
if args.site_origin is not None
else os.environ.get("SITE_ORIGIN", DEFAULT_SITE_ORIGIN)
)
out = Path(args.out).resolve()
if out == ROOT.resolve() or out.parent == out:
@@ -190,7 +208,16 @@ def main() -> None:
shutil.rmtree(out)
out.mkdir(parents=True)
for name in ("index.html", "router.js", "app.js", "style.css", "mobile-gate.js", "_headers"):
for name in (
"index.html",
"router.js",
"app.js",
"style.css",
"mobile-gate.js",
"_headers",
"_redirects",
"robots.txt",
):
src = WEB_DIR / name
if src.is_file():
shutil.copy2(src, out / name)
@@ -205,6 +232,7 @@ def main() -> None:
ability_video_base=video_base,
static_asset_base=static_base,
site_version=SITE_VERSION,
site_origin=site_origin,
)
payload = build_payload()
@@ -217,6 +245,14 @@ def main() -> None:
separators=(",", ":"),
)
template_html = (WEB_DIR / "index.html").read_text(encoding="utf-8")
seo_counts = write_seo_bundle(
out,
template_html,
payload,
site_origin=site_origin,
)
if static_base:
counts = {
name: 0
@@ -244,12 +280,17 @@ def main() -> None:
print(f"exported static site -> {out}")
for name, n in counts.items():
print(f" {name}/: {n} files")
print(
f" seo prerender: top={seo_counts['top']} "
f"heroes={seo_counts['heroes']} mechanics={seo_counts['mechanics']} "
f"(+ sitemap.xml / llms.txt / robots.txt)"
)
if n_functions:
print(f" functions/: {n_functions} files (Pages Functions)")
if args.with_videos:
print(f" ability-video/: {n_videos} files")
print(
f" config.js SITE_VERSION={SITE_VERSION!r} "
f" config.js SITE_VERSION={SITE_VERSION!r} SITE_ORIGIN={site_origin!r} "
f"ABILITY_VIDEO_BASE={video_base!r} STATIC_ASSET_BASE={static_base!r}"
)
print(f" total: {total / 1e6:.1f} MB")