Files

93 lines
4.2 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.
/* 静态预渲染:把 LandingPage 渲染为静态 HTML 注入 dist/index.html 的 #root,
让 Google / Bing / ChatGPT / Perplexity 等不执行 JS 的爬虫也能读到真实正文。
同时生成 robots.txt 与 sitemap.xml。 */
const esbuild = require('esbuild');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const fs = require('fs');
const path = require('path');
const FRONT = '/app/frontend';
const OUT = path.join(FRONT, 'dist');
// 1) 用 esbuild 把组件打包成 CJS(react/react-dom/react-router-dom 保持 external,复用运行时同一实例)
esbuild.buildSync({
entryPoints: [path.join(FRONT, 'prerender-entry.tsx')],
bundle: true,
format: 'cjs',
platform: 'node',
outfile: path.join(FRONT, 'prerender-bundle.cjs'),
alias: { '@': path.join(FRONT, 'src') },
loader: { '.css': 'empty', '.tsx': 'tsx', '.ts': 'ts' },
jsx: 'automatic',
external: ['react', 'react-dom', 'react-router-dom'],
logLevel: 'info',
});
// 2) 服务端渲染为静态标记
const mod = require(path.join(FRONT, 'prerender-bundle.cjs'));
const ssrHtml = ReactDOMServer.renderToStaticMarkup(React.createElement(mod.App));
console.log('[prerender] ssrHtml length =', ssrHtml.length);
// 3) 注入 dist/index.html 的 #root
let html = fs.readFileSync(path.join(OUT, 'index.html'), 'utf8');
const rootOpen = '<div id="root"></div>';
if (html.includes(rootOpen)) {
html = html.replace(rootOpen, '<div id="root">' + ssrHtml + '</div>');
} else {
html = html.replace(/<div id="root">[\s\S]*?<\/div>\s*<\/body>/,
'<div id="root">' + ssrHtml + '</div></body>');
}
// 4) SEO/GEO <head> 元数据:替换已有 title/description(避免重复),再注入 og/canonical/JSON-LD
const title = '七虎AI · 个人体验 AI、办公提效与 AI 创收的一站式平台';
const desc = '七虎AI 是面向个人与职场用户的 AI 平台:低价订阅无限 Token、垂直办公模板、可交易技能市场、AI 创作试炼场与低成本养模型,让每个人用 AI 把事做成。';
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: '七虎AI',
applicationCategory: 'BusinessApplication',
operatingSystem: 'Web',
offers: { '@type': 'Offer', price: '0', priceCurrency: 'CNY' },
aggregateRating: { '@type': 'AggregateRating', ratingValue: '4.8', reviewCount: '1000' },
url: 'https://ai.qihuyun.com/',
};
// 替换/新增 title 与 description(避免与源模板重复)
html = html.replace(/<title>[\s\S]*?<\/title>/, ` <title>${title}</title>`);
if (/<meta name="description"/.test(html)) {
html = html.replace(/<meta name="description"[^>]*>/, ` <meta name="description" content="${desc}" />`);
} else {
html = html.replace('</head>', ` <meta name="description" content="${desc}" />\n </head>`);
}
// 注入 og / canonical / JSON-LD(仅在尚不存在时,防止重复注入)
const extra = [
` <meta property="og:title" content="${title}" />`,
` <meta property="og:description" content="${desc}" />`,
` <meta property="og:type" content="website" />`,
` <meta name="twitter:card" content="summary_large_image" />`,
` <link rel="canonical" href="https://ai.qihuyun.com/" />`,
` <script type="application/ld+json">${JSON.stringify(jsonLd)}</script>`,
].join('\n');
if (!html.includes('rel="canonical"')) {
html = html.replace('</head>', extra + '\n </head>');
}
fs.writeFileSync(path.join(OUT, 'index.html'), html);
// 5) robots.txt
fs.writeFileSync(path.join(OUT, 'robots.txt'),
'User-agent: *\nAllow: /\n\nSitemap: https://ai.qihuyun.com/sitemap.xml\n');
// 6) sitemap.xml(当前公开可索引页仅首页;其余为登录后页面,按需在后续迭代补充)
const urls = ['https://ai.qihuyun.com/'];
const sitemap = '<?xml version="1.0" encoding="UTF-8"?>\n' +
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n' +
urls.map(u => ` <url><loc>${u}</loc><changefreq>daily</changefreq><priority>1.0</priority></url>`).join('\n') +
'\n</urlset>\n';
fs.writeFileSync(path.join(OUT, 'sitemap.xml'), sitemap);
// 7) 清理临时文件
[path.join(FRONT, 'prerender-entry.tsx'), path.join(FRONT, 'prerender-bundle.cjs')].forEach(f => fs.existsSync(f) && fs.unlinkSync(f));
console.log('PRERENDER DONE');