#!/usr/bin/env node /* C-05 SEO 预渲染:构建后脚本(无重依赖,纯 Node)。 * 数据源:与后端 seed_tools.py 同源的工具清单(url_path/name/description 必须对齐种子)。 * 产出(dist/ 内): * - 每个工具页 /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, '"') } function pageHtml({ title, desc, kw, url, h1, body, extra = '' }) { const canon = `${BASE}${url}` return ` ${esc(title)}

${esc(h1)}

${esc(body)}

${extra}

在可乐工具箱中打开使用

` } function main() { if (!existsSync(SHELL)) { console.error(`[seo] shell 不存在:${SHELL},请先跑 vite build`) process.exit(1) } const t0 = Date.now() const toolLinks = TOOLS.map((t) => `
  • ${esc(t.name)} - ${esc(t.desc)}
  • `).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: `` }), '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) => `
  • ${esc(x.name)}
  • `).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: `

    相关工具

    \n`, }), '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 = `\n\n` + urls.map((u) => ` ${esc(BASE + u)}${today}weekly${u === '/' ? '1.0' : u.startsWith('/utility/') || u.startsWith('/qrcode') || u === '/baidu-translate' || u === '/color-picker' ? '0.8' : '0.6'}`).join('\n') + `\n\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()