93 lines
4.2 KiB
JavaScript
93 lines
4.2 KiB
JavaScript
/* 静态预渲染:把 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');
|