feat(C-05):SEO预渲染+依赖瘦身+i18n补齐

build链追加seo预渲染(19工具页静态HTML+sitemap+robots)与seo:check验收;剔除chakra/emotion/@types/qrcode死依赖(pnpm-lock同步,手写qrcode.d.ts顶上);6语言locales补齐新功能文案。附带vite/nginx代理段与C-01同口径微调。e01-visual.spec.ts仅归档不跑(需8756/5173真机)。
This commit is contained in:
chunyu
2026-09-15 15:26:56 +08:00
parent af18085f40
commit e397fae928
12 changed files with 1635 additions and 1544 deletions
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env node
/* C-05 SEO 验收:扫描 dist 校验 title/description/sitemap 覆盖。
* 用法:node scripts/check_seo.mjs [--dist dist]
* 退出码:0 全绿;1 有失败(打印明细)。
*/
import { readFileSync, existsSync, readdirSync } from 'node:fs'
import { join, resolve } from 'node:path'
const args = process.argv.slice(2)
const distIdx = args.indexOf('--dist')
const DIST = resolve(distIdx >= 0 && args[distIdx + 1] ? args[distIdx + 1] : 'dist')
const EXPECT_TOOLS = [
'/baidu-translate', '/utility/json-formatter', '/utility/timestamp', '/utility/base64',
'/utility/encoding-converter', '/utility/regex', '/utility/code-formatter', '/utility/color-converter',
'/qrcode-generator', '/utility/text-diff', '/utility/calculator', '/utility/date-calculator',
'/utility/image-compressor', '/color-picker', '/utility/charset-converter', '/utility/image-editor',
]
const EXPECT_PAGES = ['/', '/utility', '/top']
let fail = 0
const ok = (cond, msg) => {
console.log(`${cond ? 'PASS' : 'FAIL'} ${msg}`)
if (!cond) fail++
}
const titleOf = (html) => (html.match(/<title>([\s\S]*?)<\/title>/) || [])[1] || ''
const descOf = (html) => (html.match(/<meta name="description" content="([\s\S]*?)"\s*\/?>/) || [])[1] || ''
// 1. 工具页 HTML 含正文 + 唯一 title/description
const titles = new Set()
for (const u of EXPECT_TOOLS) {
const f = join(DIST, u.replace(/^\//, ''), 'index.html')
ok(existsSync(f), `${u} -> dist 产物存在`)
if (!existsSync(f)) continue
const html = readFileSync(f, 'utf-8')
const t = titleOf(html), d = descOf(html)
ok(t.length > 0, `${u} title 非空(${t.slice(0, 24)}…)`)
ok(!titles.has(t), `${u} title 唯一`)
titles.add(t)
ok(d.length > 0, `${u} description 非空`)
ok(html.includes('<h1>') && html.includes('免费在线工具'), `${u} 含静态正文(h1+说明,非空#root)`)
ok(html.includes('og:title') && html.includes('og:description'), `${u} OG 标签齐全`)
}
// 2. 聚合页
for (const u of EXPECT_PAGES) {
const f = u === '/' ? join(DIST, 'index.seo.html') : join(DIST, u.replace(/^\//, ''), 'index.html')
ok(existsSync(f), `${u} -> 聚合产物存在`)
if (!existsSync(f)) continue
const html = readFileSync(f, 'utf-8')
ok(titleOf(html).length > 0 && descOf(html).length > 0, `${u} title/description 非空`)
}
// 3. sitemap/robots
const sm = join(DIST, 'sitemap.xml')
ok(existsSync(sm), 'sitemap.xml 存在')
if (existsSync(sm)) {
const body = readFileSync(sm, 'utf-8')
const missing = [...EXPECT_TOOLS, ...EXPECT_PAGES].filter((u) => !body.includes(`<loc>`) || !body.includes(u))
ok(missing.length === 0, `sitemap 覆盖 ${EXPECT_TOOLS.length + EXPECT_PAGES.length} URL${missing.length ? '(缺:' + missing.join(',') + ')' : ''}`)
}
const robots = join(DIST, 'robots.txt')
ok(existsSync(robots), 'robots.txt 存在')
if (existsSync(robots)) {
const body = readFileSync(robots, 'utf-8')
ok(body.includes('Sitemap:'), 'robots 指向 sitemap')
ok(body.includes('Disallow: /profile') && body.includes('Disallow: /wallet/'), 'robots 屏蔽登录态页')
}
// 4. SPA 外壳未被破坏
const shell = join(DIST, 'index.html')
ok(existsSync(shell) && readFileSync(shell, 'utf-8').includes('id="root"'), 'SPA 外壳 index.html 完好(含 #root)')
// 5. 种子对齐:TOOLS 数量 == 后端启用工具数(硬编码 16,漂移即报)
ok(EXPECT_TOOLS.length === 16, `工具清单 16 条(与 seed_tools 启用数对齐)`)
console.log(fail === 0 ? `\nSEO CHECK ALL GREEN (${EXPECT_TOOLS.length} tools + ${EXPECT_PAGES.length} pages)` : `\nSEO CHECK FAILED: ${fail} 项`)
process.exit(fail === 0 ? 0 : 1)