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

133 lines
9.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 预渲染:构建后脚本(无重依赖,纯 Node)。
* 数据源:与后端 seed_tools.py 同源的工具清单(url_path/name/description 必须对齐种子)。
* 产出(dist/ 内):
* - 每个工具页 <url_path>/index.html:独立 title/description/OG + 含正文的静态 HTML
* - / /utility /top 三个聚合页的静态 HTML
* - sitemap.xml(全部工具 URL + 聚合页)+ robots.txt
* 用法:npm run build 后自动执行(package.json build 链);也可单独 node scripts/seo_prerender.mjs --dist dist [--base https://www.example.com]
* 注意:base 默认为占位,部署时用 --base 传入真实域名重跑本脚本即可刷新 sitemap/OG。
*/
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
const args = process.argv.slice(2)
const distIdx = args.indexOf('--dist')
const baseIdx = args.indexOf('--base')
const DIST = resolve(distIdx >= 0 && args[distIdx + 1] ? args[distIdx + 1] : 'dist')
const BASE = (baseIdx >= 0 && args[baseIdx + 1] ? args[baseIdx + 1] : 'https://tools.example.com').replace(/\/$/, '')
const SHELL = join(DIST, 'index.html')
// 与 chunyu_project/tool/management/commands/seed_tools.py 同源(url_path/name/description 对齐)。
// is_new 仅用于正文徽标,不影响收录。
const TOOLS = [
{ name: '万能翻译', en: 'Online Translator - 200+ Languages', url: '/baidu-translate', desc: '支持200+语种互译,文本翻译、语种识别、图片翻译、语音翻译', kw: '在线翻译,语种识别,文本翻译,免费翻译工具' },
{ name: 'JSON格式化', en: 'JSON Formatter & Validator', url: '/utility/json-formatter', desc: 'JSON数据格式化、压缩、校验', kw: 'JSON格式化,JSON压缩,JSON校验,JSON在线工具' },
{ name: '时间戳转换', en: 'Unix Timestamp Converter', url: '/utility/timestamp', desc: 'Unix时间戳与日期时间互转', kw: '时间戳转换,Unix时间戳,日期互转,在线时间工具' },
{ name: 'Base64编解码', en: 'Base64 Encoder & Decoder', url: '/utility/base64', desc: 'Base64编码与解码工具', kw: 'Base64编码,Base64解码,在线Base64工具' },
{ name: '编码转换器', en: 'URL/Base64/Unicode/Hex Encoder', url: '/utility/encoding-converter', desc: '多种编码格式互转,支持URL/Base64/Unicode/Hex/Binary等', kw: 'URL编码,Unicode转换,Hex转换,在线编码工具' },
{ name: '正则表达式测试', en: 'Online Regex Tester', url: '/utility/regex', desc: '在线正则表达式测试与验证', kw: '正则测试,在线正则,Regex tester,正则验证' },
{ name: '代码格式化', en: 'Code Formatter - JS/TS/Python/HTML/CSS', url: '/utility/code-formatter', desc: '多语言代码格式化工具,支持JS/TS/Python/HTML/CSS等', kw: '代码格式化,JS格式化,Python格式化,在线代码美化' },
{ name: '颜色转换器', en: 'HEX/RGB/HSL Color Converter', url: '/utility/color-converter', desc: 'HEX、RGB、HSL颜色格式转换', kw: '颜色转换,HEX转RGB,HSL,在线取色换算' },
{ name: '二维码生成', en: 'Free QR Code Generator', url: '/qrcode-generator', desc: '生成自定义样式二维码', kw: '二维码生成,免费二维码,在线QR Code' },
{ name: '文本对比', en: 'Online Text Diff Tool', url: '/utility/text-diff', desc: '文本差异对比工具', kw: '文本对比,文本差异,在线diff工具' },
{ name: '计算器', en: 'Online Scientific Calculator', url: '/utility/calculator', desc: '科学计算器', kw: '在线计算器,科学计算器,免费计算工具' },
{ name: '日期计算器', en: 'Date Calculator - Days Between Dates', url: '/utility/date-calculator', desc: '日期间隔计算与推算', kw: '日期计算,日期间隔,天数计算,在线日期工具' },
{ name: '图片压缩', en: 'Online Image Compressor', url: '/utility/image-compressor', desc: '在线图片压缩工具', kw: '图片压缩,在线压缩图片,免费图片压缩' },
{ name: '取色器', en: 'Color Picker - HEX/RGB/HSL', url: '/color-picker', desc: '颜色拾取、HEX/RGB/HSL转换、历史记录', kw: '取色器,在线取色,HEX RGB转换' },
{ name: '字符编码转换', en: 'GBK/UTF-8 Charset Converter', url: '/utility/charset-converter', desc: 'GBK/UTF-8/Big5/Shift_JIS 等字符集互转,支持文件上传下载', kw: '字符集转换,GBK UTF8互转,编码转换工具' },
{ name: '图片编辑', en: 'Online Photo Editor - Crop & Filters', url: '/utility/image-editor', desc: '在线图片编辑,支持裁剪、滤镜、调节、文字、画笔', kw: '在线图片编辑,图片裁剪,图片滤镜' },
]
// 聚合页(首页 / 工具列表 / 热榜)
const PAGES = [
{ url: '/', title: '可乐开发工具箱 - 免费在线开发者工具', desc: '一站式免费开发者工具:JSON格式化、时间戳转换、Base64、正则测试、二维码生成、图片压缩等16款在线工具,开箱即用。', kw: '开发者工具,在线工具,免费工具箱,JSON格式化,时间戳' , h1: '可乐开发工具箱', body: '16 款免费在线开发者工具:JSON格式化、时间戳转换、Base64编解码、正则测试、二维码生成、图片压缩、取色器等,打开即用,无需安装。'},
{ url: '/utility', title: '全部在线工具 - 可乐开发工具箱', desc: '浏览全部16款免费在线工具:开发、设计、实用分类齐全,JSON/时间戳/编码/图片/颜色工具一站配齐。', kw: '在线工具列表,全部工具,免费开发工具', h1: '全部在线工具', body: '按开发、设计、实用分类浏览 16 款免费在线工具,每款工具均可直接打开使用。' },
{ url: '/top', title: '工具热榜 - 大家都在用的在线工具', desc: '可乐工具热榜:按真实使用量排序,发现最受欢迎的JSON格式化、二维码生成、时间戳转换等工具。', kw: '工具热榜,热门工具,常用在线工具', h1: '工具热榜', body: '按近7天真实使用量排序的工具热榜,快速找到大家都在用的在线工具。' },
]
function esc(s) {
return String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
}
function pageHtml({ title, desc, kw, url, h1, body, extra = '' }) {
const canon = `${BASE}${url}`
return `<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${esc(title)}</title>
<meta name="description" content="${esc(desc)}" />
<meta name="keywords" content="${esc(kw)}" />
<link rel="canonical" href="${esc(canon)}" />
<meta property="og:type" content="website" />
<meta property="og:title" content="${esc(title)}" />
<meta property="og:description" content="${esc(desc)}" />
<meta property="og:url" content="${esc(canon)}" />
<meta name="robots" content="index,follow" />
</head>
<body>
<main>
<h1>${esc(h1)}</h1>
<p>${esc(body)}</p>
${extra}
<p><a href="${esc(canon)}">在可乐工具箱中打开使用</a></p>
</main>
<div id="root"></div>
<script>location.replace("${esc(url)}");</script>
<noscript><p>请启用 JavaScript 以使用完整交互功能。</p></noscript>
</body>
</html>`
}
function main() {
if (!existsSync(SHELL)) {
console.error(`[seo] shell 不存在:${SHELL},请先跑 vite build`)
process.exit(1)
}
const t0 = Date.now()
const toolLinks = TOOLS.map((t) => `<li><a href="${esc(BASE + t.url)}">${esc(t.name)}</a> - ${esc(t.desc)}</li>`).join('\n')
let count = 0
for (const p of PAGES) {
const out = join(DIST, p.url === '/' ? 'index.seo.html' : p.url.replace(/^\//, '') + '/index.html')
void out
// 首页 SEO 正文直接写 heritage 文件: dist/index.seo.html 供 nginx 优先返回(不覆盖 SPA 外壳 index.html)
const target = p.url === '/'
? join(DIST, 'index.seo.html')
: join(DIST, p.url.replace(/^\//, ''), 'index.html')
mkdirSync(dirname(target), { recursive: true })
writeFileSync(target, pageHtml({ ...p, extra: `<ul>\n${toolLinks}\n</ul>` }), 'utf-8')
count++
}
for (const t of TOOLS) {
const target = join(DIST, t.url.replace(/^\//, ''), 'index.html')
mkdirSync(dirname(target), { recursive: true })
const others = TOOLS.filter((x) => x.url !== t.url).slice(0, 8)
.map((x) => `<li><a href="${esc(BASE + x.url)}">${esc(x.name)}</a></li>`).join('\n')
writeFileSync(target, pageHtml({
title: `${t.name} - 免费在线工具 | 可乐开发工具箱`,
desc: `${t.desc}。免费在线${t.name}工具,打开即用,无需下载安装。`,
kw: t.kw,
url: t.url,
h1: `${t.name} - 免费在线工具`,
body: `${t.desc}。${t.en}。`,
extra: `<h2>相关工具</h2>\n<ul>\n${others}\n</ul>`,
}), 'utf-8')
count++
}
// sitemap.xml:聚合页 + 全部工具页
const urls = [...PAGES.map((p) => p.url), ...TOOLS.map((t) => t.url)]
const today = new Date().toISOString().slice(0, 10)
const sm = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n` +
urls.map((u) => ` <url><loc>${esc(BASE + u)}</loc><lastmod>${today}</lastmod><changefreq>weekly</changefreq><priority>${u === '/' ? '1.0' : u.startsWith('/utility/') || u.startsWith('/qrcode') || u === '/baidu-translate' || u === '/color-picker' ? '0.8' : '0.6'}</priority></url>`).join('\n') +
`\n</urlset>\n`
writeFileSync(join(DIST, 'sitemap.xml'), sm, 'utf-8')
writeFileSync(join(DIST, 'robots.txt'), `User-agent: *\nAllow: /\nDisallow: /profile\nDisallow: /wallet/\nDisallow: /console\nDisallow: /messages\nDisallow: /chat\nSitemap: ${BASE}/sitemap.xml\n`, 'utf-8')
console.log(`[seo] prerender done: ${count} html + sitemap(${urls.length} urls) + robots.txt in ${Date.now() - t0}ms -> ${DIST}`)
}
main()