259 lines
10 KiB
JavaScript
259 lines
10 KiB
JavaScript
// build.js
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const data = require('./data.js');
|
|
|
|
const template = fs.readFileSync(path.join(__dirname, 'template.html'), 'utf-8');
|
|
|
|
// 清理并创建 dist 目录
|
|
const distDir = path.join(__dirname, 'dist');
|
|
if (fs.existsSync(distDir)) {
|
|
fs.rmSync(distDir, { recursive: true, force: true });
|
|
}
|
|
fs.mkdirSync(distDir);
|
|
|
|
// 创建子目录
|
|
const projectSubDir = path.join(distDir, data.siteConfig.basePath.replace(/^\//, ''));
|
|
if (!fs.existsSync(projectSubDir)) {
|
|
fs.mkdirSync(projectSubDir, { recursive: true });
|
|
}
|
|
|
|
// 拷贝 assets 目录
|
|
const assetsDir = path.join(__dirname, 'assets');
|
|
const targetAssetsDir = path.join(projectSubDir, 'assets');
|
|
if (fs.existsSync(assetsDir)) {
|
|
if (!fs.existsSync(targetAssetsDir)) fs.mkdirSync(targetAssetsDir, { recursive: true });
|
|
fs.cpSync(assetsDir, targetAssetsDir, { recursive: true });
|
|
console.log('✅ Assets 已同步');
|
|
}
|
|
|
|
data.languages.forEach(lang => {
|
|
const content = data.content[lang];
|
|
let html = template;
|
|
|
|
const fullBasePath = data.siteConfig.basePath;
|
|
const fullDomain = data.siteConfig.domain;
|
|
|
|
html = html.replace(/{{langCode}}/g, lang);
|
|
html = html.replace(/{{title}}/g, content.title);
|
|
html = html.replace(/{{metaDesc}}/g, content.metaDesc);
|
|
|
|
// SEO Dynamic Tags
|
|
const canonicalUrl = `${fullDomain}${fullBasePath}/${lang}/index.html`;
|
|
html = html.replace(/{{canonicalUrl}}/g, canonicalUrl);
|
|
|
|
// Generate hreflang tags
|
|
const hreflangTags = data.languages.map(l => {
|
|
return `<link rel="alternate" hreflang="${l}" href="${fullDomain}${fullBasePath}/${l}/index.html" />`;
|
|
}).join('\n ');
|
|
// Add x-default (usually English)
|
|
const xDefault = `<link rel="alternate" hreflang="x-default" href="${fullDomain}${fullBasePath}/en/index.html" />`;
|
|
html = html.replace(/{{hreflangTags}}/g, hreflangTags + '\n ' + xDefault);
|
|
|
|
// Use a default OG image or generate one (placeholder for now)
|
|
html = html.replace(/{{ogImage}}/g, `${fullDomain}${fullBasePath}/assets/og-shared.jpg`);
|
|
|
|
// 语言链接 - 动态生成 (保持相对路径,兼容性更好)
|
|
const langLinksHtml = data.languages.map(l => {
|
|
const lContent = data.content[l];
|
|
if (l === lang) return `<span class="active-lang">${lContent.langName}</span>`;
|
|
return `<a href="../${l}/index.html">${lContent.langName}</a>`;
|
|
}).join('');
|
|
html = html.replace(/{{languageLinks}}/g, langLinksHtml);
|
|
|
|
// 表头
|
|
const headersHtml = content.headers.map(h => `<th>${h}</th>`).join('');
|
|
html = html.replace(/{{tableHeaders}}/g, headersHtml);
|
|
|
|
// 表格行 + 详情行
|
|
const rowsHtml = content.cities.map((city, idx) => `
|
|
<tr id="row-${idx}" class="city-row" onclick="toggleDetails(${idx})">
|
|
<td>
|
|
<div class="city-name-cell">
|
|
<strong>${city.name}</strong>
|
|
<span class="tag-region">${city.region}</span>
|
|
</div>
|
|
</td>
|
|
<td>${city.matches}</td>
|
|
<td style="font-weight:600">${city.budget}</td>
|
|
<td><span class="tag-visa ${city.vCls}">${city.visa}</span></td>
|
|
<td style="color:#64748b; font-size:13px">${city.tips}</td>
|
|
</tr>
|
|
<tr id="details-${idx}" class="detail-pane">
|
|
<td colspan="5">
|
|
<div class="detail-content">
|
|
<div class="info-box">
|
|
<h4>${content.ui_stadium}</h4>
|
|
<div class="info-text">${city.std}</div>
|
|
</div>
|
|
<div class="info-box">
|
|
<h4>${content.ui_logistics}</h4>
|
|
<div class="info-text">${city.log}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
|
|
html = html.replace(/{{tableRows}}/g, rowsHtml);
|
|
|
|
const outputDir = path.join(projectSubDir, lang);
|
|
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
|
fs.writeFileSync(path.join(outputDir, 'index.html'), html);
|
|
console.log(`✅ ${lang} 版生成完毕 (输出至: ${data.siteConfig.basePath}/${lang}/)`);
|
|
});
|
|
|
|
// 生成 sitemap.xml
|
|
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
<url>
|
|
<loc>${data.siteConfig.domain}/</loc>
|
|
<lastmod>${new Date().toISOString().split('T')[0]}</lastmod>
|
|
<priority>1.00</priority>
|
|
</url>
|
|
${data.languages.map(lang => `
|
|
<url>
|
|
<loc>${data.siteConfig.domain}${data.siteConfig.basePath}/${lang}/index.html</loc>
|
|
<lastmod>${new Date().toISOString().split('T')[0]}</lastmod>
|
|
<priority>0.80</priority>
|
|
</url>`).join('')}
|
|
</urlset>`;
|
|
fs.writeFileSync(path.join(__dirname, 'dist', 'sitemap.xml'), sitemap);
|
|
console.log('✅ sitemap.xml 已生成');
|
|
|
|
// 生成 robots.txt
|
|
const robots = `User-agent: *
|
|
Allow: /
|
|
|
|
Sitemap: ${data.siteConfig.domain}${data.siteConfig.basePath}/sitemap.xml
|
|
`;
|
|
fs.writeFileSync(path.join(__dirname, 'dist', 'robots.txt'), robots);
|
|
console.log('✅ robots.txt 已生成');
|
|
|
|
// 拷贝 ads.txt (如果存在)
|
|
const adsFile = path.join(__dirname, 'ads.txt');
|
|
if (fs.existsSync(adsFile)) {
|
|
fs.copyFileSync(adsFile, path.join(__dirname, 'dist', 'ads.txt'));
|
|
console.log('✅ ads.txt 已同步到 dist');
|
|
}
|
|
|
|
// 生成根目录首页 (PickBetterAI.com 门面)
|
|
const homeHtml = `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Pick Better AI - Smart Decisions Enhanced by AI</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Outfit:wght@700;800&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--bg: #030712;
|
|
--card-bg: rgba(17, 24, 39, 0.7);
|
|
--accent: #6366f1;
|
|
--text: #f9fafb;
|
|
--text-muted: #9ca3af;
|
|
}
|
|
body {
|
|
font-family: 'Inter', sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
margin: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 100vh;
|
|
background-image: radial-gradient(circle at top right, rgba(99, 102, 241, 0.15), transparent),
|
|
radial-gradient(circle at bottom left, rgba(168, 85, 247, 0.15), transparent);
|
|
}
|
|
.container {
|
|
max-width: 1000px;
|
|
width: 90%;
|
|
padding: 40px 0;
|
|
}
|
|
header { text-align: center; margin-bottom: 60px; }
|
|
h1 { font-family: 'Outfit', sans-serif; font-size: 48px; margin: 0; background: linear-gradient(to right, #fff, #9ca3af); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
|
header p { color: var(--text-muted); font-size: 18px; margin-top: 10px; }
|
|
|
|
.bento-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
grid-gap: 24px;
|
|
}
|
|
.card {
|
|
background: var(--card-bg);
|
|
backdrop-filter: blur(12px);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 24px;
|
|
padding: 30px;
|
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.3s;
|
|
text-decoration: none;
|
|
color: inherit;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
}
|
|
.card:hover { transform: translateY(-5px); border-color: var(--accent); }
|
|
.card.featured { grid-column: span 2; grid-row: span 2; background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(168, 85, 247, 0.1)); border: 1px solid rgba(99, 102, 241, 0.3); }
|
|
.card h2 { font-size: 24px; margin: 0 0 10px 0; font-family: 'Outfit', sans-serif; }
|
|
.card p { color: var(--text-muted); font-size: 14px; line-height: 1.6; margin: 0; }
|
|
.card .icon { font-size: 40px; margin-bottom: 20px; }
|
|
.tag { display: inline-block; padding: 4px 12px; border-radius: 100px; font-size: 10px; font-weight: 700; text-transform: uppercase; background: var(--accent); color: white; margin-bottom: 12px; }
|
|
.tag.soon { background: #374151; color: #9ca3af; }
|
|
|
|
@media (max-width: 768px) {
|
|
.bento-grid { grid-template-columns: 1fr; }
|
|
.card.featured { grid-column: span 1; grid-row: span 1; }
|
|
h1 { font-size: 32px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<div style="font-family: 'Outfit', sans-serif; font-weight: 800; font-size: 24px; color: var(--accent); margin-bottom: 20px;">PickBetterAI</div>
|
|
<h1>Smart Decisions, Enhanced by AI</h1>
|
|
<p>We help you find the best options in travel, tech, and life.</p>
|
|
</header>
|
|
|
|
<div class="bento-grid">
|
|
<a href="${data.siteConfig.basePath}/en/index.html" class="card featured">
|
|
<div class="tag">Active Tool</div>
|
|
<div class="icon">⚽️</div>
|
|
<div>
|
|
<h2>2026 World Cup City Planner</h2>
|
|
<p>Compare 16 host cities across USA, Canada, and Mexico. Decide where to watch based on match count, budget, and local logistics. Multi-language support included.</p>
|
|
</div>
|
|
</a>
|
|
|
|
<a href="https://games.pickbetterai.com" class="card">
|
|
<div class="tag">Launching</div>
|
|
<div class="icon">🎮</div>
|
|
<h2>AI Games</h2>
|
|
<p>A curated collection of the best browser games, powered by Game Distribution.</p>
|
|
</a>
|
|
|
|
<div class="card">
|
|
<div class="tag soon">Coming Soon</div>
|
|
<div class="icon">💻</div>
|
|
<h2>AI Tech Selection</h2>
|
|
<p>Picking the right AI model or tool for your specific workflow.</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="tag soon">Coming Soon</div>
|
|
<div class="icon">✈️</div>
|
|
<h2>Smart Travel</h2>
|
|
<p>Data-driven destination comparison for digital nomads.</p>
|
|
</div>
|
|
|
|
<div class="card" style="grid-column: span 3; padding: 20px; text-align: center; background: transparent; border: none;">
|
|
<p>© 2026 PickBetterAI.com | All Rights Reserved</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>`;
|
|
|
|
fs.writeFileSync(path.join(__dirname, 'dist', 'index.html'), homeHtml);
|
|
console.log('✅ 根目录首页 index.html 已生成');
|