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真机)。
133 lines
9.6 KiB
JavaScript
133 lines
9.6 KiB
JavaScript
#!/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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
||
}
|
||
|
||
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()
|