fix(i18n): localePath 三处双 en 一刀治+canonical 全量(blog-5f T2/T3 车道收编)
- T2:localePath 先归一 zh 再拼前缀(Header/CommandPalette/
BaseLayout 三处 /en/en/ 同源病根一刀),返回值统一尾斜杠;
BaseLayout 自指 canonical 全量 35 页;E2E 新增 en canonical
自指断言(33→34)
- T3:content-lint.mjs 零依赖三检(断链 FAIL/缺图 FAIL/zh-en
配对 WARN;CONTENT_LINT 逃生门;photos-originals 叠加层与
/admin 单语两误报已修);prod-smoke.sh 三断言(robots 未落明
确 FAIL 不静默);build.sh 守卫行挂载(smoke 对 staging 首版
容忍,T1 robots 上线后转硬门,触发条件注释在案)
- tasks.md verify 字面量 33→34 联动同步(6 处)
车道:op-gitea(verify 六条实录);收编验收:E2E 34/34+产物零
/en/en/+lint 0/0/0 实跑复现
验证: node tests/e2e.mjs 34/34 PASS;! grep -r /en/en/ dist/;
content-lint 三检 0 违例;bash -n+test -x 过
Spec: 2026-10-26_blog-5f
Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
8ec1712135
commit
c307285a25
7 files changed
+131
-9
No files matched your search
@@ -13,7 +13,7 @@
|
|||||||
- [ ] T2 localePath 双 en 修复 + 自指 canonical + 尾斜杠归一
|
- [ ] T2 localePath 双 en 修复 + 自指 canonical + 尾斜杠归一
|
||||||
- claim: op-gitea(mesh 车道)
|
- claim: op-gitea(mesh 车道)
|
||||||
- boundary: src/lib/locale.ts(先归一 zh 再拼前缀);BaseLayout.astro 加自指 canonical(Astro.site+withSlash);Header.astro nav-lang href 尾斜杠补齐;E2E 可加断言
|
- boundary: src/lib/locale.ts(先归一 zh 再拼前缀);BaseLayout.astro 加自指 canonical(Astro.site+withSlash);Header.astro nav-lang href 尾斜杠补齐;E2E 可加断言
|
||||||
- verify: `! grep -r "/en/en/" dist/ && grep -q 'rel="canonical"' dist/en/archives/mian-sha/index.html && node tests/e2e.mjs 2>&1 | grep -q "33/33 PASS"`
|
- verify: `! grep -r "/en/en/" dist/ && grep -q 'rel="canonical"' dist/en/archives/mian-sha/index.html && node tests/e2e.mjs 2>&1 | grep -q "34/34 PASS"`
|
||||||
|
|
||||||
- [ ] T3 构建期内容 lint + 生产冒烟 + build.sh 守卫挂载
|
- [ ] T3 构建期内容 lint + 生产冒烟 + build.sh 守卫挂载
|
||||||
- claim: op-gitea(mesh 车道)
|
- claim: op-gitea(mesh 车道)
|
||||||
@@ -23,24 +23,24 @@
|
|||||||
- [ ] T4 reader 纯文本模式(html.reader + 命令面板 + r 键 + 持久化)
|
- [ ] T4 reader 纯文本模式(html.reader + 命令面板 + r 键 + 持久化)
|
||||||
- claim: orchestrator
|
- claim: orchestrator
|
||||||
- boundary: global.css 尾部集中 html.reader 段(按 08 文档 §4.4 清单:隐 bg-glow/进度条/收 Header 尾部钮/Footer 收单行 ICP 必留;正文字号微升行宽收窄);CommandPalette data-cmd="reader" + ui.ts cmd_reader 双语键;Keyboard r 单键;localStorage 持久化;reduced-motion/无 JS 天然兼容(纯 class)
|
- boundary: global.css 尾部集中 html.reader 段(按 08 文档 §4.4 清单:隐 bg-glow/进度条/收 Header 尾部钮/Footer 收单行 ICP 必留;正文字号微升行宽收窄);CommandPalette data-cmd="reader" + ui.ts cmd_reader 双语键;Keyboard r 单键;localStorage 持久化;reduced-motion/无 JS 天然兼容(纯 class)
|
||||||
- verify: `node tests/e2e.mjs 2>&1 | grep -q "33/33 PASS" && grep -q 'data-cmd="reader"' src/components/CommandPalette.astro && grep -q "html.reader" src/styles/global.css`
|
- verify: `node tests/e2e.mjs 2>&1 | grep -q "34/34 PASS" && grep -q 'data-cmd="reader"' src/components/CommandPalette.astro && grep -q "html.reader" src/styles/global.css`
|
||||||
|
|
||||||
- [ ] T5 代码块窗条(P 卡:≥4 行灰阶窗+mono 标题+灰阶三点)
|
- [ ] T5 代码块窗条(P 卡:≥4 行灰阶窗+mono 标题+灰阶三点)
|
||||||
- claim: op-artalk(mesh 车道)
|
- claim: op-artalk(mesh 车道)
|
||||||
- boundary: 技术轨代码块渲染层(自查源码定落点:prose-tech 容器/全局 pre 样式);≥4 行块加窗条(灰阶三点非红黄绿+mono 语言/文件标签),单行块裸 pill;亮暗双态;scoped 样式优先不动 global.css(与 T7 划界)
|
- boundary: 技术轨代码块渲染层(自查源码定落点:prose-tech 容器/全局 pre 样式);≥4 行块加窗条(灰阶三点非红黄绿+mono 语言/文件标签),单行块裸 pill;亮暗双态;scoped 样式优先不动 global.css(与 T7 划界)
|
||||||
- verify: `node tests/e2e.mjs 2>&1 | grep -q "33/33 PASS" && grep -rqE "code-win|codebar|窗条" src/styles/ src/components/`
|
- verify: `node tests/e2e.mjs 2>&1 | grep -q "34/34 PASS" && grep -rqE "code-win|codebar|窗条" src/styles/ src/components/`
|
||||||
|
|
||||||
- [ ] T6 书评纸面板(Q 卡:圆角米灰纸卡+暗色暖棕+hairline 边框)
|
- [ ] T6 书评纸面板(Q 卡:圆角米灰纸卡+暗色暖棕+hairline 边框)
|
||||||
- claim: op-artalk(mesh 车道)
|
- claim: op-artalk(mesh 车道)
|
||||||
- boundary: 书评正文容器(prose-reading 所在组件 scoped 样式):圆角纸卡+米灰 #f4f4f2 系亮/暖棕暗态;边框 hairline 级防长文竖线感(08 裁定 §6-2);标题区外置结构不动;--paper 变量需求提给 orchestrator 入 tokens(T7 协同),不直接改 tokens.css
|
- boundary: 书评正文容器(prose-reading 所在组件 scoped 样式):圆角纸卡+米灰 #f4f4f2 系亮/暖棕暗态;边框 hairline 级防长文竖线感(08 裁定 §6-2);标题区外置结构不动;--paper 变量需求提给 orchestrator 入 tokens(T7 协同),不直接改 tokens.css
|
||||||
- verify: `node tests/e2e.mjs 2>&1 | grep -q "33/33 PASS" && grep -rq "paper" src/components/ | grep -v node_modules`
|
- verify: `node tests/e2e.mjs 2>&1 | grep -q "34/34 PASS" && grep -rq "paper" src/components/ | grep -v node_modules`
|
||||||
|
|
||||||
- [ ] T7 版式基底(K/L/O 卡:流距 token+灰阶补档+暗色 hairline 降档+微交互曲线+focus ring)
|
- [ ] T7 版式基底(K/L/O 卡:流距 token+灰阶补档+暗色 hairline 降档+微交互曲线+focus ring)
|
||||||
- claim: orchestrator(tokens/global 专属车道)
|
- claim: orchestrator(tokens/global 专属车道)
|
||||||
- boundary: tokens.css(--flow-* 流距五变量+灰阶补档评估+--paper 纸色变量);global.css(h2/h3/p+p 改用流距变量、暗色 hairline 降一档、过渡曲线统一 token :where 一段、focus-visible ring 统一);标题字重 700→600(裁定 #6);行距 +2~4px(裁定 #7);移动端密集扫描复测(涉布局面)
|
- boundary: tokens.css(--flow-* 流距五变量+灰阶补档评估+--paper 纸色变量);global.css(h2/h3/p+p 改用流距变量、暗色 hairline 降一档、过渡曲线统一 token :where 一段、focus-visible ring 统一);标题字重 700→600(裁定 #6);行距 +2~4px(裁定 #7);移动端密集扫描复测(涉布局面)
|
||||||
- verify: `node tests/e2e.mjs 2>&1 | grep -q "33/33 PASS" && grep -q "flow-h2-pre" src/styles/tokens.css && grep -q "focus-visible" src/styles/global.css`
|
- verify: `node tests/e2e.mjs 2>&1 | grep -q "34/34 PASS" && grep -q "flow-h2-pre" src/styles/tokens.css && grep -q "focus-visible" src/styles/global.css`
|
||||||
|
|
||||||
- [ ] T8 首屏 tagline + 图库质感(M/N 卡:身份语+圆角统一+暗色衬底)
|
- [ ] T8 首屏 tagline + 图库质感(M/N 卡:身份语+圆角统一+暗色衬底)
|
||||||
- claim: orchestrator
|
- claim: orchestrator
|
||||||
- boundary: 首页身份区(IdCard 或等位组件)加 tagline ≤20 字(措辞备两案入完成总结供用户选,默认「写作者 · 读者 · 快门按压者」);GalleryPage 圆角统一 --radius-img 暗色 4% 衬底(裁定 §6-4 双模式分判;衬底圆角≤照片圆角防框中框);亮色直贴不动
|
- boundary: 首页身份区(IdCard 或等位组件)加 tagline ≤20 字(措辞备两案入完成总结供用户选,默认「写作者 · 读者 · 快门按压者」);GalleryPage 圆角统一 --radius-img 暗色 4% 衬底(裁定 §6-4 双模式分判;衬底圆角≤照片圆角防框中框);亮色直贴不动
|
||||||
- verify: `node tests/e2e.mjs 2>&1 | grep -q "33/33 PASS" && grep -rq "tagline" src/pages/index.astro src/components/ && grep -q "radius-img" src/styles/tokens.css`
|
- verify: `node tests/e2e.mjs 2>&1 | grep -q "34/34 PASS" && grep -rq "tagline" src/pages/index.astro src/components/ && grep -q "radius-img" src/styles/tokens.css`
|
||||||
@@ -31,6 +31,9 @@ pnpm astro build
|
|||||||
[ -f scripts/build-perf/gen-sw-precache.mjs ] && node scripts/build-perf/gen-sw-precache.mjs dist || echo "[build] sw-precache 生成器未就绪,跳过"
|
[ -f scripts/build-perf/gen-sw-precache.mjs ] && node scripts/build-perf/gen-sw-precache.mjs dist || echo "[build] sw-precache 生成器未就绪,跳过"
|
||||||
[ -f scripts/build-perf/inline-home-css.mjs ] && node scripts/build-perf/inline-home-css.mjs || echo "[build] inline-home 脚本未就绪,跳过"
|
[ -f scripts/build-perf/inline-home-css.mjs ] && node scripts/build-perf/inline-home-css.mjs || echo "[build] inline-home 脚本未就绪,跳过"
|
||||||
|
|
||||||
|
# 内容 lint(T3):断链/缺图在部署前拦截;CONTENT_LINT=skip|warn 逃生门见脚本头注
|
||||||
|
[ -f scripts/build-perf/content-lint.mjs ] && node scripts/build-perf/content-lint.mjs dist || echo "[build] content-lint 未就绪,跳过"
|
||||||
|
|
||||||
echo "[build] rsync -> $STAGING"
|
echo "[build] rsync -> $STAGING"
|
||||||
mkdir -p "$STAGING"
|
mkdir -p "$STAGING"
|
||||||
rsync -a --delete "$BUILD_REPO/dist/" "$STAGING/"
|
rsync -a --delete "$BUILD_REPO/dist/" "$STAGING/"
|
||||||
@@ -38,3 +41,9 @@ ORIG="$HOME/docker_apps/blog/photos-originals"
|
|||||||
if [ -d "$ORIG" ]; then rsync -a "$ORIG/" "$STAGING/photos-originals/"; fi
|
if [ -d "$ORIG" ]; then rsync -a "$ORIG/" "$STAGING/photos-originals/"; fi
|
||||||
printf 'built %s at %s\n' "$SHA" "$(date -Is)" > "$STAGING/.build-stamp"
|
printf 'built %s at %s\n' "$SHA" "$(date -Is)" > "$STAGING/.build-stamp"
|
||||||
echo "[build] done: $(find "$STAGING" -name '*.html' | wc -l) html files @ $SHA"
|
echo "[build] done: $(find "$STAGING" -name '*.html' | wc -l) html files @ $SHA"
|
||||||
|
|
||||||
|
# 生产冒烟(T3):staging 三断言。首版容忍失败(robots 待 T1 落地,smoke FAIL 属预期),
|
||||||
|
# T1 收编后改硬门:去掉 `|| echo` 让断言失败直接炸构建
|
||||||
|
if [ -f scripts/build-perf/prod-smoke.sh ] && [ "${SMOKE_BASE:-http://127.0.0.1:18743}" != "off" ]; then
|
||||||
|
bash scripts/build-perf/prod-smoke.sh "${SMOKE_BASE:-http://127.0.0.1:18743}" || echo "[build] prod-smoke 未过(见上;T1 robots 落地后转硬门)"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// 构建期内容 lint(T3,零依赖):扫 dist 产物三检——站内断链 / 图片存在 / zh↔en 配对
|
||||||
|
// 用法:node scripts/build-perf/content-lint.mjs [dist目录=dist]
|
||||||
|
// 逃生门:CONTENT_LINT=skip 整体跳过;CONTENT_LINT=warn 只警告不炸构建(首版兼容期)
|
||||||
|
// 口径:断链/缺图 = FAIL(exit 1);配对缺失 = WARN(en 缺页可能是有意的,人工裁决)
|
||||||
|
import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
|
||||||
|
import { join, resolve as pathResolve } from 'node:path';
|
||||||
|
|
||||||
|
const DIST = pathResolve(process.argv[2] ?? 'dist');
|
||||||
|
const MODE = process.env.CONTENT_LINT ?? 'fail';
|
||||||
|
if (MODE === 'skip') {
|
||||||
|
console.log('[content-lint] CONTENT_LINT=skip,整体跳过');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SKIP_PREFIX = ['/admin/', '/photos-originals/']; // admin=CMS 路由;photos-originals=部署期 rsync 叠加层(不在 dist,另有部署链保障)
|
||||||
|
const SKIP_FILE = new Set(['/404.html']); // Astro 404 页不参与配对
|
||||||
|
|
||||||
|
function walk(dir, out = []) {
|
||||||
|
for (const e of readdirSync(dir)) {
|
||||||
|
const p = join(dir, e);
|
||||||
|
if (statSync(p).isDirectory()) walk(p, out);
|
||||||
|
else if (e.endsWith('.html')) out.push(p);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** /x/ → dist/x/index.html;/a.svg → dist/a.svg;找不到即 null */
|
||||||
|
function targetExists(url) {
|
||||||
|
if (SKIP_PREFIX.some((s) => url.startsWith(s))) return true;
|
||||||
|
const p = url.split('#')[0].split('?')[0];
|
||||||
|
if (!p || p === '/') return existsSync(join(DIST, 'index.html'));
|
||||||
|
if (p.endsWith('/')) return existsSync(join(DIST, p, 'index.html'));
|
||||||
|
return existsSync(join(DIST, p)) || existsSync(join(DIST, `${p}.html`)) || existsSync(join(DIST, p, 'index.html'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = walk(DIST);
|
||||||
|
const fails = [];
|
||||||
|
const warns = [];
|
||||||
|
|
||||||
|
for (const f of files) {
|
||||||
|
const html = readFileSync(f, 'utf8');
|
||||||
|
const rel = f.slice(DIST.length) || '/';
|
||||||
|
|
||||||
|
// 站内 <a href>:仅根相对路径;#锚点/mailto/tel/外部/data: 全跳过
|
||||||
|
for (const m of html.matchAll(/<a\s[^>]*href="([^"]*)"/g)) {
|
||||||
|
const href = m[1];
|
||||||
|
if (!href.startsWith('/') || href.startsWith('//')) continue;
|
||||||
|
if (!targetExists(href)) fails.push(`断链 ${rel} → ${href}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// <img src> 与 srcset:产物必须存在(构建期副本检查,防部署后 404 图)
|
||||||
|
for (const m of html.matchAll(/<img\s[^>]*>/g)) {
|
||||||
|
const tag = m[0];
|
||||||
|
const src = /(?:\ssrc="([^"]*)")/.exec(tag)?.[1];
|
||||||
|
const srcset = /(?:\ssrcset="([^"]*)")/.exec(tag)?.[1];
|
||||||
|
for (const u of [src, ...(srcset ?? '').split(',').map((s) => s.trim().split(/\s+/)[0])]) {
|
||||||
|
if (!u || !u.startsWith('/') || u.startsWith('//')) continue;
|
||||||
|
if (!targetExists(u)) fails.push(`缺图 ${rel} → ${u}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// zh↔en 配对:dist/en/x/index.html ↔ dist/x/index.html,双向 WARN
|
||||||
|
if (!SKIP_FILE.has(rel) && !SKIP_PREFIX.some((s) => rel.startsWith(s))) {
|
||||||
|
const other = rel.startsWith('/en/') ? rel.slice(3) : `/en${rel === '/index.html' ? '/index.html' : rel}`;
|
||||||
|
if (!existsSync(join(DIST, other))) warns.push(`配对缺 ${rel} ↔ ${other}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const w of warns) console.warn(`[content-lint] WARN ${w}`);
|
||||||
|
if (fails.length) {
|
||||||
|
for (const x of fails) console.error(`[content-lint] FAIL ${x}`);
|
||||||
|
if (MODE === 'warn') {
|
||||||
|
console.warn(`[content-lint] WARN 模式:${fails.length} 个失败降级为警告`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
console.error(`[content-lint] ${fails.length} 失败 / ${warns.length} 警告`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(`[content-lint] 通过:${files.length} 页扫描,0 断链 0 缺图,${warns.length} 配对警告`);
|
||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 生产冒烟(T3):部署后三断言——robots 带 Sitemap / sitemap-index 可达 / 首页无 x-robots-tag
|
||||||
|
# 用法:./prod-smoke.sh [BASE_URL](默认 https://wait4x.online;构建机对 staging 传 http://127.0.0.1:18743)
|
||||||
|
# 任一断言失败 exit 1;robots 缺失常见于 SEO 解封前(T1 未落),会明确报 FAIL 而非静默
|
||||||
|
set -u
|
||||||
|
BASE="${1:-${SMOKE_BASE:-https://wait4x.online}}"
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
robots="$(curl -fsS --max-time 15 "${BASE}/robots.txt" 2>/dev/null)" \
|
||||||
|
&& grep -q 'Sitemap:' <<<"$robots" \
|
||||||
|
&& echo "[smoke] robots.txt 200 含 Sitemap ✓" \
|
||||||
|
|| { echo "[smoke] FAIL robots.txt 缺失/无 Sitemap 行:${BASE}/robots.txt"; fail=1; }
|
||||||
|
|
||||||
|
curl -fsS --max-time 15 -o /dev/null "${BASE}/sitemap-index.xml" \
|
||||||
|
&& echo "[smoke] sitemap-index 200 ✓" \
|
||||||
|
|| { echo "[smoke] FAIL sitemap-index 不可达:${BASE}/sitemap-index.xml"; fail=1; }
|
||||||
|
|
||||||
|
# x-robots-tag 出现即 FAIL(noindex 头会废掉整站收录,SEO 解封的反面)
|
||||||
|
if curl -fsS --max-time 15 -I "${BASE}/" | grep -qi 'x-robots-tag'; then
|
||||||
|
echo "[smoke] FAIL 首页响应头含 x-robots-tag"; fail=1
|
||||||
|
else
|
||||||
|
echo "[smoke] 首页无 x-robots-tag ✓"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $fail
|
||||||
@@ -30,6 +30,8 @@ const rawPath = Astro.url.pathname.replace(/\/index\.html$/, '');
|
|||||||
const zhPath = localeOf(rawPath) === 'en' ? localePath(rawPath, 'zh') : rawPath;
|
const zhPath = localeOf(rawPath) === 'en' ? localePath(rawPath, 'zh') : rawPath;
|
||||||
const enPath = localePath(rawPath, 'en');
|
const enPath = localePath(rawPath, 'en');
|
||||||
const withSlash = (p: string) => (p.endsWith('/') ? p : `${p}/`);
|
const withSlash = (p: string) => (p.endsWith('/') ? p : `${p}/`);
|
||||||
|
// 自指 canonical(T2):SEO 权重收敛到当前页自身,与 hreflang 组合覆盖双轨
|
||||||
|
const canonicalUrl = new URL(withSlash(rawPath), Astro.site).href;
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
@@ -87,6 +89,7 @@ const withSlash = (p: string) => (p.endsWith('/') ? p : `${p}/`);
|
|||||||
addEventListener('load', () => navigator.serviceWorker.register('/sw.js'));
|
addEventListener('load', () => navigator.serviceWorker.register('/sw.js'));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<link rel="canonical" href={canonicalUrl} />
|
||||||
<link rel="alternate" hreflang="zh" href={new URL(withSlash(zhPath), Astro.site)} />
|
<link rel="alternate" hreflang="zh" href={new URL(withSlash(zhPath), Astro.site)} />
|
||||||
<link rel="alternate" hreflang="en" href={new URL(withSlash(enPath), Astro.site)} />
|
<link rel="alternate" hreflang="en" href={new URL(withSlash(enPath), Astro.site)} />
|
||||||
<link rel="alternate" hreflang="x-default" href={new URL(withSlash(zhPath), Astro.site)} />
|
<link rel="alternate" hreflang="x-default" href={new URL(withSlash(zhPath), Astro.site)} />
|
||||||
|
|||||||
+6
-3
@@ -7,9 +7,12 @@ export type Locale = 'zh' | 'en';
|
|||||||
export const localeOf = (pathname: string): Locale =>
|
export const localeOf = (pathname: string): Locale =>
|
||||||
pathname === '/en' || pathname.startsWith('/en/') ? 'en' : 'zh';
|
pathname === '/en' || pathname.startsWith('/en/') ? 'en' : 'zh';
|
||||||
|
|
||||||
/** 当前路径在另一语言下的对应路径(zh → /en/...,en → 去 /en 前缀)。 */
|
/** 当前路径在另一语言下的对应路径(zh → /en/...,en → 去 /en 前缀)。
|
||||||
|
* 尾斜杠归一:返回值一律带尾斜杠(与全站/sitemap 口径一致)。
|
||||||
|
* 先归一 zh 再拼前缀——en→en 场景若直接 /en+clean 会产出 /en/en/(T2 病根) */
|
||||||
export function localePath(pathname: string, to: Locale): string {
|
export function localePath(pathname: string, to: Locale): string {
|
||||||
const clean = pathname.replace(/\/index\.html$/, '').replace(/\/$/, '') || '/';
|
const clean = pathname.replace(/\/index\.html$/, '').replace(/\/$/, '') || '/';
|
||||||
if (to === 'en') return clean === '/' ? '/en/' : `/en${clean}`;
|
const zh = clean.replace(/^\/en(?=\/|$)/, '') || '/';
|
||||||
return clean.replace(/^\/en(?=\/|$)/, '') || '/';
|
const slash = (p: string) => (p === '/' ? '/' : `${p}/`);
|
||||||
|
return to === 'en' ? slash(zh === '/' ? '/en' : `/en${zh}`) : slash(zh);
|
||||||
}
|
}
|
||||||
@@ -86,6 +86,8 @@ const enNavBad = await page.$$eval('nav a[href^="/"]:not(.nav-lang)', (as) => as
|
|||||||
ok('en 导航全轨 /en/ 前缀', enNavBad.length === 0 && enNavHrefs.length >= 6 && enNavHrefs.includes('/en/books/'), enNavBad.join(' ; ') || enNavHrefs.join(' '));
|
ok('en 导航全轨 /en/ 前缀', enNavBad.length === 0 && enNavHrefs.length >= 6 && enNavHrefs.includes('/en/books/'), enNavBad.join(' ; ') || enNavHrefs.join(' '));
|
||||||
const enBrand = await page.getAttribute('a.brand', 'href');
|
const enBrand = await page.getAttribute('a.brand', 'href');
|
||||||
ok('en 品牌链接指 /en/', enBrand === '/en/', String(enBrand));
|
ok('en 品牌链接指 /en/', enBrand === '/en/', String(enBrand));
|
||||||
|
const enCanon = await page.getAttribute('link[rel="canonical"]', 'href');
|
||||||
|
ok('en 页 canonical 自指', !!enCanon && new URL(enCanon).pathname === new URL(page.url()).pathname, enCanon ?? '无 canonical');
|
||||||
|
|
||||||
// 4) 命令面板 + 搜索
|
// 4) 命令面板 + 搜索
|
||||||
await page.keyboard.press('Control+k');
|
await page.keyboard.press('Control+k');
|
||||||
|
|||||||
Reference in new issue
Block a user