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
@@ -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
|
||||
Reference in new issue
Block a user