Files
chunyu e397fae928 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真机)。
2026-09-15 15:26:56 +08:00

80 lines
3.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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)