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:
+137
-2
@@ -1,4 +1,4 @@
|
||||
/* global fetch, document, ABILITY_VIDEO_BASE, STATIC_ASSET_BASE, SITE_VERSION */
|
||||
/* global fetch, document, ABILITY_VIDEO_BASE, STATIC_ASSET_BASE, SITE_VERSION, SITE_ORIGIN */
|
||||
|
||||
function trimBase(raw) {
|
||||
return typeof raw === "string" ? raw.trim().replace(/\/+$/, "") : "";
|
||||
@@ -76,6 +76,138 @@ function siteVersionLabel() {
|
||||
return v ? `v${v}` : "";
|
||||
}
|
||||
|
||||
function siteOrigin() {
|
||||
const configured = typeof SITE_ORIGIN === "string" ? SITE_ORIGIN.trim().replace(/\/+$/, "") : "";
|
||||
if (configured) return configured;
|
||||
if (typeof location !== "undefined" && location.origin) return location.origin;
|
||||
return "";
|
||||
}
|
||||
|
||||
const PAGE_SEO_LABELS = {
|
||||
heroes: "英雄克制与搭档",
|
||||
rankings: "Immortal 排行",
|
||||
streamers: "主播",
|
||||
matches: "明星比赛",
|
||||
trends: "近 8 周走势",
|
||||
mechanics: "机制查询",
|
||||
items: "物品商店",
|
||||
patches: "版本更新",
|
||||
};
|
||||
|
||||
const DETAIL_TAB_SEO_LABELS = {
|
||||
skills: "技能",
|
||||
core: "核心装",
|
||||
fears: "怕的装备",
|
||||
trends: "走势",
|
||||
matchups: "对位",
|
||||
matches: "近期比赛",
|
||||
streamers: "主播",
|
||||
patches: "版本改动",
|
||||
};
|
||||
|
||||
function setMetaByKey(attr, key, content) {
|
||||
if (!content) return;
|
||||
let el = document.querySelector(`meta[${attr}="${key}"]`);
|
||||
if (!el) {
|
||||
el = document.createElement("meta");
|
||||
el.setAttribute(attr, key);
|
||||
document.head.appendChild(el);
|
||||
}
|
||||
el.setAttribute("content", content);
|
||||
}
|
||||
|
||||
function describeStateForSeo(st) {
|
||||
const brand = "上分帝";
|
||||
const page = st.page || "heroes";
|
||||
let title = `${PAGE_SEO_LABELS[page] || "DOTA2"} — ${brand}`;
|
||||
let description =
|
||||
"Dota 2 英雄机制克制与搭档、段位走势、机制查询、物品与版本更新。";
|
||||
let path = "/" + page;
|
||||
try {
|
||||
if (typeof serializeHash === "function") path = serializeHash(st) || "/heroes";
|
||||
} catch (_) {
|
||||
/* keep path */
|
||||
}
|
||||
if (!st.data) return { title, description, path };
|
||||
|
||||
if (page === "heroes" && st.selectedKey) {
|
||||
const hero = heroByKey(st.selectedKey);
|
||||
const name = (hero && hero.name_loc) || st.selectedKey;
|
||||
const aliases = (hero && hero.aliases) || [];
|
||||
const tab = DETAIL_TAB_SEO_LABELS[st.detailTab] || "详情";
|
||||
title = `${name} ${tab} / 克制搭档 — ${brand}`;
|
||||
const aliasBit = aliases.length ? `(${aliases.slice(0, 3).join("、")})` : "";
|
||||
description = `${name}${aliasBit}的 Dota 2 机制克制、被克制与搭档参考,以及技能、出装、走势与对位数据。`;
|
||||
} else if (page === "mechanics") {
|
||||
const mq = st.data.mechanic_query || {};
|
||||
const labels = mq.labels || {};
|
||||
const effect = st.mechanicEffect || "basic_dispel";
|
||||
const label = labels[effect] || effect;
|
||||
title = `${label} — 机制查询 — ${brand}`;
|
||||
const blurb = (mq.blurbs && mq.blurbs[effect]) || "";
|
||||
description = blurb || `查询 Dota 2 中施加「${label}」的技能与物品。`;
|
||||
} else if (page === "items" && st.selectedItemKey) {
|
||||
const meta = shopItem(st.selectedItemKey);
|
||||
const name = (meta && (meta.name_loc || meta.dname)) || st.selectedItemKey;
|
||||
title = `${name} — 物品 — ${brand}`;
|
||||
description = `${name} 的合成、描述与机制标签(上分帝物品页)。`;
|
||||
} else if (page === "patches") {
|
||||
const ver =
|
||||
st.selectedPatch ||
|
||||
((st.data.patches && st.data.patches[0]) || {}).version;
|
||||
if (ver) {
|
||||
title = `版本 ${ver} — ${brand}`;
|
||||
description = `Dota 2 ${ver} 游戏性更新摘要(上分帝版本页)。`;
|
||||
}
|
||||
} else if (page === "trends") {
|
||||
title = `近 8 周走势 — ${brand}`;
|
||||
description = "各勋章段位近 8 周英雄胜率与上场率走势榜。";
|
||||
} else if (page === "rankings") {
|
||||
title = `Immortal 排行榜 — ${brand}`;
|
||||
description = "Valve Immortal 四区 Top100 选手榜。";
|
||||
} else if (page === "streamers") {
|
||||
title = `Dota 2 主播 — ${brand}`;
|
||||
description = "精选 Dota 2 主播目录与高光(抖音 / B 站 / 斗鱼)。";
|
||||
} else if (page === "matches") {
|
||||
title = `明星比赛 — ${brand}`;
|
||||
description = "明星选手近期职业与国服对局、终局出装与加点。";
|
||||
} else if (page === "heroes") {
|
||||
title = `英雄克制与搭档 — ${brand}`;
|
||||
description =
|
||||
"按英雄浏览定性克制 / 被克制 / 搭档理由,以及技能、核心装、走势与对位。";
|
||||
}
|
||||
|
||||
return { title, description, path };
|
||||
}
|
||||
|
||||
function updateDocumentMeta() {
|
||||
if (typeof document === "undefined" || !document.title) return;
|
||||
const { title, description, path } = describeStateForSeo(state);
|
||||
document.title = title;
|
||||
setMetaByKey("name", "description", description);
|
||||
setMetaByKey("property", "og:title", title);
|
||||
setMetaByKey("property", "og:description", description);
|
||||
setMetaByKey("name", "twitter:title", title);
|
||||
setMetaByKey("name", "twitter:description", description);
|
||||
const origin = siteOrigin();
|
||||
if (origin) {
|
||||
const url = origin + (path.startsWith("/") ? path : `/${path}`);
|
||||
setMetaByKey("property", "og:url", url);
|
||||
let link = document.querySelector('link[rel="canonical"]');
|
||||
if (!link) {
|
||||
link = document.createElement("link");
|
||||
link.setAttribute("rel", "canonical");
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
link.setAttribute("href", url);
|
||||
}
|
||||
}
|
||||
|
||||
function clearSeoPrerender() {
|
||||
const el = document.getElementById("seo-prerender");
|
||||
if (el) el.remove();
|
||||
}
|
||||
|
||||
function renderPatchesSiteVersion() {
|
||||
const el = $("#patches-site-version");
|
||||
if (!el) return;
|
||||
@@ -5468,6 +5600,7 @@ function render() {
|
||||
} else {
|
||||
renderPatches();
|
||||
}
|
||||
updateDocumentMeta();
|
||||
}
|
||||
|
||||
// Search-box URL sync timers (debounced replace so typing does not spam history).
|
||||
@@ -5698,7 +5831,8 @@ async function main() {
|
||||
if (brandLogo) brandLogo.src = assetUrl("/ui-icon/dota2_logo_wordmark.png");
|
||||
const favicon = document.querySelector('link[rel="icon"]');
|
||||
if (favicon) favicon.href = assetUrl("/ui-icon/dota2_logo.png");
|
||||
const res = await fetch("data.json");
|
||||
// Absolute path: History routes like /heroes/axe must not resolve data.json relatively.
|
||||
const res = await fetch("/data.json");
|
||||
if (!res.ok) throw new Error(`data.json: HTTP ${res.status}`);
|
||||
state.data = await res.json();
|
||||
state.data.relations = state.data.relations || { counters: [], synergies: [] };
|
||||
@@ -5755,6 +5889,7 @@ async function main() {
|
||||
_innateAbilityKeys = null;
|
||||
installRouter({ getState: () => state, applyPatch });
|
||||
applyUrlToState();
|
||||
clearSeoPrerender();
|
||||
refreshStreamerLiveStatus();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user