31cc1e02ce
- 新增 AffiliateProgram / AffiliateLink / AffiliateClick 规范化 Prisma 模型 取代原手写裸表 affiliate_stats / affiliate_clicks - 新增迁移 prisma/migrations/20260711000000_add_affiliate_models - 重构 affiliate.service.ts 改用 Prisma ORM,消除 $queryRawUnsafe SQL 注入 - 重构 affiliate.controller.ts 接口:programs / links(?skillId,?toolId) / stats / click - 前端 affiliate 页接入真实接口,移除硬编码 demo 数据 - 技能详情页新增「学此技能推荐使用的工具」联盟链接区块 - 新增 affiliate.service.spec.ts(4 用例通过)与幂等种子 seed-affiliate.ts - 更新 docs/progress/current.md,明确无 ICP 经营许可证下以联盟返佣为合规变现主路径 - 含此前工作区未提交改动(工具 slug 路由、支付/订单、SEO 等) Co-Authored-By: opencode <opencode@anthropic.com>
101 lines
3.3 KiB
JavaScript
101 lines
3.3 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const SITE_URL = 'https://yuzhiran.com';
|
|
const OUT_DIR = path.join(__dirname, '..', 'out');
|
|
|
|
const PRIORITIES = {
|
|
'/': 1.0,
|
|
'/tools': 0.9,
|
|
'/courses': 0.9,
|
|
'/skills': 0.8,
|
|
'/marketplace': 0.8,
|
|
'/practices': 0.8,
|
|
'/contents': 0.8,
|
|
'/prompts': 0.8,
|
|
'/prompts/workshop': 0.7,
|
|
'/models': 0.7,
|
|
'/sandbox': 0.7,
|
|
'/sandbox/compare': 0.6,
|
|
'/sandbox/code': 0.6,
|
|
'/sandbox/share': 0.6,
|
|
'/discover': 0.7,
|
|
'/discover/hot': 0.6,
|
|
'/discover/tools': 0.6,
|
|
'/discover/daily': 0.6,
|
|
'/community': 0.6,
|
|
'/circles': 0.6,
|
|
'/resources': 0.7,
|
|
'/about': 0.5,
|
|
'/privacy': 0.3,
|
|
'/terms': 0.3,
|
|
'/ai-agreement': 0.3,
|
|
'/search': 0.5,
|
|
};
|
|
|
|
function walkDir(dir, basePath = '') {
|
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
const urls = [];
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(dir, entry.name);
|
|
const relativeName = path.join(basePath, entry.name);
|
|
if (entry.isDirectory()) {
|
|
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
|
|
urls.push(...walkDir(fullPath, relativeName));
|
|
} else if (entry.name === 'page.html' || entry.name.endsWith('.html')) {
|
|
if (relativeName === 'index.html' || relativeName === 'page.html') {
|
|
urls.push({ path: '/', file: fullPath });
|
|
} else {
|
|
let urlPath = '/' + relativeName.replace(/\.html$/, '').replace(/\/index$/, '').replace(/\/page$/, '');
|
|
urls.push({ path: urlPath, file: fullPath });
|
|
}
|
|
}
|
|
}
|
|
return urls;
|
|
}
|
|
|
|
function generateSitemap(urls) {
|
|
const now = new Date().toISOString().split('T')[0];
|
|
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';
|
|
const seen = new Set();
|
|
for (const url of urls) {
|
|
if (seen.has(url.path)) continue;
|
|
seen.add(url.path);
|
|
if (url.path.startsWith('/admin') || url.path.startsWith('/_next') || url.path.startsWith('/api') || url.path === '/404') continue;
|
|
if (url.path.includes('/[') || url.path.includes(']')) continue;
|
|
const depth = url.path.split('/').filter(Boolean).length;
|
|
const changefreq = depth <= 1 ? 'weekly' : 'monthly';
|
|
const priority = PRIORITIES[url.path];
|
|
const computedPriority = priority !== undefined ? priority : Math.max(0.1, 0.7 - depth * 0.1);
|
|
xml += ' <url>\n';
|
|
xml += ` <loc>${SITE_URL}${url.path}</loc>\n`;
|
|
xml += ` <lastmod>${now}</lastmod>\n`;
|
|
xml += ` <changefreq>${changefreq}</changefreq>\n`;
|
|
xml += ` <priority>${computedPriority.toFixed(1)}</priority>\n`;
|
|
xml += ' </url>\n';
|
|
}
|
|
xml += '</urlset>';
|
|
return xml;
|
|
}
|
|
|
|
if (!fs.existsSync(OUT_DIR)) {
|
|
console.error('out/ directory not found. Run next build first.');
|
|
process.exit(1);
|
|
}
|
|
|
|
const urls = walkDir(OUT_DIR);
|
|
console.log(`Found ${urls.length} HTML pages in output.`);
|
|
const sitemap = generateSitemap(urls);
|
|
const outputPath = path.join(OUT_DIR, 'sitemap.xml');
|
|
fs.writeFileSync(outputPath, sitemap, 'utf-8');
|
|
console.log(`Generated sitemap.xml with ${sitemap.split('<url>').length - 1} URLs → ${outputPath}`);
|
|
|
|
// Copy robots.txt from public/ to out/
|
|
const robotsSrc = path.join(__dirname, '..', 'public', 'robots.txt');
|
|
const robotsDst = path.join(OUT_DIR, 'robots.txt');
|
|
if (fs.existsSync(robotsSrc)) {
|
|
fs.copyFileSync(robotsSrc, robotsDst);
|
|
console.log(`Copied robots.txt → ${robotsDst}`);
|
|
}
|