update
This commit is contained in:
@@ -101,6 +101,10 @@ for page in HTML_PAGES:
|
|||||||
rel = page.relative_to(SITE).as_posix()
|
rel = page.relative_to(SITE).as_posix()
|
||||||
text = page.read_text(encoding="utf-8")
|
text = page.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
# 重定向页(不带 .html 的入口)只有一个跳转,不需要完整的页面结构
|
||||||
|
if 'http-equiv="refresh"' in text:
|
||||||
|
continue
|
||||||
|
|
||||||
# doctype
|
# doctype
|
||||||
if not text.lstrip().lower().startswith("<!doctype html"):
|
if not text.lstrip().lower().startswith("<!doctype html"):
|
||||||
report("结构", rel, "缺少 <!DOCTYPE html>")
|
report("结构", rel, "缺少 <!DOCTYPE html>")
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ TUTORIALS_CANDIDATES = (
|
|||||||
PYPI_DIR / "tutorials",
|
PYPI_DIR / "tutorials",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 首页文件名。首页在地址栏里显示成目录(/ 与 /en/),不带 index.html
|
||||||
|
HOME_FILENAME = "index.html"
|
||||||
|
|
||||||
|
|
||||||
def e(text: object) -> str:
|
def e(text: object) -> str:
|
||||||
"""HTML 转义。"""
|
"""HTML 转义。"""
|
||||||
@@ -223,9 +226,19 @@ def lang_url(lang: str, filename: str, from_lang: str) -> str:
|
|||||||
"""另一种语言里同一个页面的相对路径(相对于当前页面)。
|
"""另一种语言里同一个页面的相对路径(相对于当前页面)。
|
||||||
|
|
||||||
中文页面在根目录,英文页面在 en/ 子目录,所以两个方向要分别处理。
|
中文页面在根目录,英文页面在 en/ 子目录,所以两个方向要分别处理。
|
||||||
|
首页用目录地址表示(/ 与 /en/),不带 index.html。
|
||||||
"""
|
"""
|
||||||
target_dir = i18n.LANG_DIRS[lang]
|
target_dir = i18n.LANG_DIRS[lang]
|
||||||
current_dir = i18n.LANG_DIRS[from_lang]
|
current_dir = i18n.LANG_DIRS[from_lang]
|
||||||
|
if filename == HOME_FILENAME:
|
||||||
|
# 首页指向目录本身,而不是 index.html
|
||||||
|
if target_dir == current_dir:
|
||||||
|
return "./"
|
||||||
|
if not target_dir:
|
||||||
|
return "../"
|
||||||
|
if not current_dir:
|
||||||
|
return target_dir + "/"
|
||||||
|
return "../" + target_dir + "/"
|
||||||
if target_dir == current_dir:
|
if target_dir == current_dir:
|
||||||
return filename
|
return filename
|
||||||
if not target_dir:
|
if not target_dir:
|
||||||
@@ -241,26 +254,21 @@ def lang_dir(output: Path, lang: str) -> Path:
|
|||||||
return output / directory if directory else output
|
return output / directory if directory else output
|
||||||
|
|
||||||
|
|
||||||
def absolute_url(base: str, path: str) -> str:
|
def absolute_page_url(base: str, lang: str, filename: str) -> str:
|
||||||
"""把站点内的相对路径拼成绝对地址。
|
"""页面的绝对地址(canonical 与 hreflang 用)。
|
||||||
|
|
||||||
base 是站点根地址(如 https://py.guanjihuan.com),path 是页面里的相对路径
|
首页用目录地址表示:/ 与 /en/,不带 index.html;
|
||||||
(如 "api.html" 或 "../api.html")。搜索引擎要求 hreflang 与 canonical
|
其余页面是 /api.html 这种带后缀的地址。
|
||||||
用绝对地址,所以这里把 ".." 之类的片段解析掉再拼。
|
不知道站点根地址时退回到相对地址,页面至少还能互相跳转。
|
||||||
不知道根地址时原样返回,页面至少还能用相对地址互相跳转。
|
|
||||||
"""
|
"""
|
||||||
|
directory = i18n.LANG_DIRS[lang]
|
||||||
|
if filename == HOME_FILENAME:
|
||||||
if not base:
|
if not base:
|
||||||
return path
|
return "./"
|
||||||
parts: list[str] = []
|
return base + "/" + (directory + "/" if directory else "")
|
||||||
for segment in path.split("/"):
|
if not base:
|
||||||
if segment in ("", "."):
|
return filename
|
||||||
continue
|
return base + "/" + "/".join(part for part in (directory, filename) if part)
|
||||||
if segment == "..":
|
|
||||||
if parts:
|
|
||||||
parts.pop()
|
|
||||||
continue
|
|
||||||
parts.append(segment)
|
|
||||||
return base + "/" + "/".join(parts)
|
|
||||||
|
|
||||||
|
|
||||||
def site_base_url(package: dict, override: str | None = None) -> str:
|
def site_base_url(package: dict, override: str | None = None) -> str:
|
||||||
@@ -287,16 +295,29 @@ def _lang_context(data: dict, lang: str, pages: list[PageSpec]) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def nav_html(pages: list[PageSpec], active_key: str, t) -> str:
|
def nav_html(pages: list[PageSpec], active_key: str, t) -> str:
|
||||||
"""生成导航条(顶栏与页脚共用)。"""
|
"""生成导航条(顶栏与页脚共用)。
|
||||||
|
|
||||||
|
首页指向当前目录(./),也就是 / 与 /en/,不带 index.html。
|
||||||
|
"""
|
||||||
items = []
|
items = []
|
||||||
for spec in pages:
|
for spec in pages:
|
||||||
class_attr = "is-active" if spec.key == active_key else ""
|
class_attr = "is-active" if spec.key == active_key else ""
|
||||||
|
href = home_href() if spec.key == "index" else spec.filename
|
||||||
items.append(
|
items.append(
|
||||||
f'<a href="{spec.filename}" class="{class_attr}">{e(t(spec.label_key))}</a>'
|
f'<a href="{href}" class="{class_attr}">{e(t(spec.label_key))}</a>'
|
||||||
)
|
)
|
||||||
return "\n ".join(items)
|
return "\n ".join(items)
|
||||||
|
|
||||||
|
|
||||||
|
def home_href() -> str:
|
||||||
|
"""首页链接(相对于当前页面所在的目录)。
|
||||||
|
|
||||||
|
页面都在自己语言的目录里(中文在根目录,英文在 en/),
|
||||||
|
所以 "./" 正好指向该语言的首页:/ 与 /en/。
|
||||||
|
"""
|
||||||
|
return "./"
|
||||||
|
|
||||||
|
|
||||||
def page(
|
def page(
|
||||||
template: str,
|
template: str,
|
||||||
*,
|
*,
|
||||||
@@ -345,11 +366,12 @@ def page(
|
|||||||
lang_switch_aria=t("switch_to_lang", lang=i18n.LANG_NAMES_FULL[other]),
|
lang_switch_aria=t("switch_to_lang", lang=i18n.LANG_NAMES_FULL[other]),
|
||||||
lang_switch_hreflang=i18n.HTML_LANG[other],
|
lang_switch_hreflang=i18n.HTML_LANG[other],
|
||||||
# hreflang 与 canonical 用绝对地址——搜索引擎只认绝对的
|
# hreflang 与 canonical 用绝对地址——搜索引擎只认绝对的
|
||||||
alt_zh=absolute_url(site_url, lang_url("zh", filename, lang)),
|
alt_zh=absolute_page_url(site_url, "zh", filename),
|
||||||
alt_en=absolute_url(site_url, lang_url("en", filename, lang)),
|
alt_en=absolute_page_url(site_url, "en", filename),
|
||||||
# x-default 指向默认语言(中文)的那一份
|
# x-default 指向默认语言(中文)的那一份
|
||||||
alt_default=absolute_url(site_url, lang_url(i18n.DEFAULT_LANG, filename, lang)),
|
alt_default=absolute_page_url(site_url, i18n.DEFAULT_LANG, filename),
|
||||||
canonical=absolute_url(site_url, lang_url(lang, filename, lang)),
|
canonical=absolute_page_url(site_url, lang, filename),
|
||||||
|
home_href=home_href(),
|
||||||
skip_link=t("skip"),
|
skip_link=t("skip"),
|
||||||
theme_label=t("theme_toggle"),
|
theme_label=t("theme_toggle"),
|
||||||
menu_label=t("menu_open"),
|
menu_label=t("menu_open"),
|
||||||
@@ -962,6 +984,62 @@ def _find_local_file(name: str, base_dirs: list[Path]) -> Path | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
REDIRECT_TEMPLATE = """<!DOCTYPE html>
|
||||||
|
<html lang="{lang}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="robots" content="noindex, follow">
|
||||||
|
<title>{title}</title>
|
||||||
|
<meta http-equiv="refresh" content="0; url={target}">
|
||||||
|
<link rel="canonical" href="{canonical}">
|
||||||
|
<script>location.replace({target_js});</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>{title}:<a href="{target}">{target}</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def write_redirect_stubs(output: Path, pages: list[PageSpec], site_url: str) -> list[str]:
|
||||||
|
"""给每个页面生成一个不带 .html 的入口。
|
||||||
|
|
||||||
|
访问 /about 时,服务器会去找 /about 这个目录里的 index.html,
|
||||||
|
找到的就是这里生成的重定向页,它会跳到 /about.html。
|
||||||
|
服务器上配了重定向规则(把 /about 直接 301 到 /about.html)时走不到这一层;
|
||||||
|
这里生成的文件是纯静态部署时的兜底。
|
||||||
|
"""
|
||||||
|
written: list[str] = []
|
||||||
|
for lang in i18n.LANGS:
|
||||||
|
directory = lang_dir(output, lang)
|
||||||
|
for spec in pages:
|
||||||
|
if spec.filename == HOME_FILENAME:
|
||||||
|
continue # 首页用目录地址(/ 与 /en/),不需要重定向
|
||||||
|
name = Path(spec.filename).stem
|
||||||
|
stub_dir = directory / name
|
||||||
|
stub_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
target = "../" + spec.filename
|
||||||
|
title = f"正在跳转到 {target}"
|
||||||
|
# 不知道站点根地址时,规范地址用相对地址(相对于这个重定向页)
|
||||||
|
canonical = (
|
||||||
|
absolute_page_url(site_url, lang, spec.filename) if site_url else target
|
||||||
|
)
|
||||||
|
stub = stub_dir / "index.html"
|
||||||
|
stub.write_text(
|
||||||
|
REDIRECT_TEMPLATE.format(
|
||||||
|
lang=i18n.HTML_LANG[lang],
|
||||||
|
title=e(title),
|
||||||
|
target=e(target),
|
||||||
|
canonical=e(canonical),
|
||||||
|
target_js=json.dumps(target),
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
written.append(stub.relative_to(output).as_posix())
|
||||||
|
return written
|
||||||
|
|
||||||
|
|
||||||
def clean_output(output: Path) -> None:
|
def clean_output(output: Path) -> None:
|
||||||
"""清空输出目录,只留下本次要生成的东西。
|
"""清空输出目录,只留下本次要生成的东西。
|
||||||
|
|
||||||
@@ -1175,6 +1253,9 @@ def generate(
|
|||||||
base_dirs.append(Path(tutorials_dir).parent)
|
base_dirs.append(Path(tutorials_dir).parent)
|
||||||
copied, missing = copy_local_assets(output, base_dirs)
|
copied, missing = copy_local_assets(output, base_dirs)
|
||||||
|
|
||||||
|
# 不带 .html 的入口:/about 也能访问,会跳到 /about.html
|
||||||
|
stubs = write_redirect_stubs(output, pages, site_url)
|
||||||
|
|
||||||
errors = [m for m in modules if m.error]
|
errors = [m for m in modules if m.error]
|
||||||
problems = verify_output(output)
|
problems = verify_output(output)
|
||||||
if not quiet:
|
if not quiet:
|
||||||
@@ -1205,6 +1286,8 @@ def generate(
|
|||||||
print(f" (共 {len(problems)} 处链接问题)")
|
print(f" (共 {len(problems)} 处链接问题)")
|
||||||
if copied:
|
if copied:
|
||||||
print(f" 已复制 {len(copied)} 个本地资源:{', '.join(sorted(set(copied)))}")
|
print(f" 已复制 {len(copied)} 个本地资源:{', '.join(sorted(set(copied)))}")
|
||||||
|
if stubs:
|
||||||
|
print(f" 已生成 {len(stubs)} 个不带 .html 的入口(跳转到对应的 .html 页面)")
|
||||||
|
|
||||||
# 供调用方判断构建是否干净
|
# 供调用方判断构建是否干净
|
||||||
data["problems"] = problems
|
data["problems"] = problems
|
||||||
|
|||||||
@@ -658,13 +658,36 @@ def check_i18n(data: dict, output: Path) -> None:
|
|||||||
check(">Modules<" in en_index, "英文页面有对应的英文标题")
|
check(">Modules<" in en_index, "英文页面有对应的英文标题")
|
||||||
|
|
||||||
print("\n语言切换")
|
print("\n语言切换")
|
||||||
check('href="en/index.html"' in zh_index, "中文页面链到英文页面")
|
check('href="en/"' in zh_index, "中文页面链到英文页面")
|
||||||
check('href="../index.html"' in en_index, "英文页面链回中文页面")
|
check('href="../"' in en_index, "英文页面链回中文页面")
|
||||||
zh_api = (output / "api.html").read_text(encoding="utf-8")
|
zh_api = (output / "api.html").read_text(encoding="utf-8")
|
||||||
en_api = (output / "en" / "api.html").read_text(encoding="utf-8")
|
en_api = (output / "en" / "api.html").read_text(encoding="utf-8")
|
||||||
check('href="en/api.html"' in zh_api, "API 页也能切到另一种语言")
|
check('href="en/api.html"' in zh_api, "API 页也能切到另一种语言")
|
||||||
check('href="../api.html"' in en_api, "英文 API 页能切回中文")
|
check('href="../api.html"' in en_api, "英文 API 页能切回中文")
|
||||||
|
|
||||||
|
print("\n地址里不带 index.html")
|
||||||
|
zh_canonical = re.search(r'canonical" href="([^"]*)"', zh_index)
|
||||||
|
en_canonical = re.search(r'canonical" href="([^"]*)"', en_index)
|
||||||
|
check(
|
||||||
|
bool(zh_canonical) and not zh_canonical.group(1).endswith("index.html"),
|
||||||
|
"中文首页的规范地址不带 index.html",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
bool(en_canonical) and not en_canonical.group(1).endswith("index.html"),
|
||||||
|
"英文首页的规范地址不带 index.html",
|
||||||
|
)
|
||||||
|
check('href="./" class="is-active"' in zh_index, "首页链接指向目录")
|
||||||
|
stub = output / "about" / "index.html"
|
||||||
|
check(stub.exists(), "生成了不带 .html 的入口(/about)")
|
||||||
|
if stub.exists():
|
||||||
|
stub_text = stub.read_text(encoding="utf-8")
|
||||||
|
check("../about.html" in stub_text, "入口跳到对应的 .html 页面")
|
||||||
|
stub_canonical = re.search(r'canonical" href="([^"]*)"', stub_text)
|
||||||
|
check(
|
||||||
|
bool(stub_canonical) and stub_canonical.group(1).endswith("about.html"),
|
||||||
|
"入口的规范地址指向 .html 页面",
|
||||||
|
)
|
||||||
|
|
||||||
print("\n文件内容保持原样")
|
print("\n文件内容保持原样")
|
||||||
# README、源码里的说明不翻译:两种语言的页面里都应当原样出现
|
# README、源码里的说明不翻译:两种语言的页面里都应当原样出现
|
||||||
zh_about = (output / "about.html").read_text(encoding="utf-8")
|
zh_about = (output / "about.html").read_text(encoding="utf-8")
|
||||||
|
|||||||
@@ -22,12 +22,23 @@
|
|||||||
else document.documentElement.setAttribute("data-theme", "dark");
|
else document.documentElement.setAttribute("data-theme", "dark");
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
</script>
|
</script>
|
||||||
|
<script>
|
||||||
|
// 首页统一用不带 index.html 的地址(/ 与 /en/)。
|
||||||
|
// 服务器配了重定向规则时用不上这里,只是没配服务器时的兜底。
|
||||||
|
(function () {
|
||||||
|
var suffix = "/index.html";
|
||||||
|
var path = location.pathname;
|
||||||
|
if (path.slice(-suffix.length) === suffix) {
|
||||||
|
location.replace(path.slice(0, path.length - "index.html".length));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<a class="skip" href="#main">{{skip_link}}</a>
|
<a class="skip" href="#main">{{skip_link}}</a>
|
||||||
<header class="site-head">
|
<header class="site-head">
|
||||||
<div class="wrap head-inner">
|
<div class="wrap head-inner">
|
||||||
<a class="brand" href="index.html">
|
<a class="brand" href="{{home_href}}">
|
||||||
<img class="brand-mark" src="static/icon.jpg" alt="" width="28" height="28">
|
<img class="brand-mark" src="static/icon.jpg" alt="" width="28" height="28">
|
||||||
<span class="brand-text">{{package_name}}</span>
|
<span class="brand-text">{{package_name}}</span>
|
||||||
<span class="brand-version">v{{version}}</span>
|
<span class="brand-version">v{{version}}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user