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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user