NebulaCloud/lib/sitemap-utils.ts

290 lines
9.1 KiB
TypeScript
Raw Normal View History

2025-09-15 17:28:58 +08:00
import { locales, defaultLocale } from './i18n';
import { getSitemapStats, generateSitemap } from './sitemap-generator';
/**
*
*/
export class SitemapValidator {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
/**
* URL 访
*/
async validateUrls(): Promise<{
valid: string[];
invalid: string[];
errors: Array<{ url: string; error: string }>;
}> {
const sitemap = generateSitemap(this.baseUrl);
const valid: string[] = [];
const invalid: string[] = [];
const errors: Array<{ url: string; error: string }> = [];
for (const entry of sitemap) {
try {
const response = await fetch(entry.url, { method: 'HEAD' });
if (response.ok) {
valid.push(entry.url);
} else {
invalid.push(entry.url);
errors.push({
url: entry.url,
error: `HTTP ${response.status}: ${response.statusText}`,
});
}
} catch (error) {
invalid.push(entry.url);
errors.push({
url: entry.url,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}
return { valid, invalid, errors };
}
/**
* URL
*/
checkDuplicates(): string[] {
const sitemap = generateSitemap(this.baseUrl);
const urls = sitemap.map((entry) => entry.url);
const duplicates: string[] = [];
const seen = new Set<string>();
for (const url of urls) {
if (seen.has(url)) {
duplicates.push(url);
} else {
seen.add(url);
}
}
return duplicates;
}
/**
*
*/
generateReport(): {
stats: ReturnType<typeof getSitemapStats>;
duplicates: string[];
recommendations: string[];
} {
const stats = getSitemapStats(this.baseUrl);
const duplicates = this.checkDuplicates();
const recommendations: string[] = [];
// 生成建议
if (duplicates.length > 0) {
recommendations.push(`发现 ${duplicates.length} 个重复的 URL请检查站点地图生成逻辑`);
}
if (stats.urlCount > 50000) {
recommendations.push('站点地图包含超过 50,000 个 URL建议分割为多个站点地图文件');
}
if (stats.blogPages === 0) {
recommendations.push('未发现博客页面,请确认博客内容是否正确配置');
}
return {
stats,
duplicates,
recommendations,
};
}
}
/**
*
*/
export class SitemapSubmitter {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
/**
* URL
*/
getSubmissionUrls(): Record<string, string> {
const sitemapUrl = `${this.baseUrl}/sitemap.xml`;
return {
google: `https://www.google.com/ping?sitemap=${encodeURIComponent(sitemapUrl)}`,
bing: `https://www.bing.com/ping?sitemap=${encodeURIComponent(sitemapUrl)}`,
yandex: `https://webmaster.yandex.com/ping?sitemap=${encodeURIComponent(sitemapUrl)}`,
baidu: `https://ping.baidu.com/ping/RPC2?sitemap=${encodeURIComponent(sitemapUrl)}`,
};
}
/**
*
*/
async submitToSearchEngines(): Promise<{
success: string[];
failed: Array<{ engine: string; error: string }>;
}> {
const urls = this.getSubmissionUrls();
const success: string[] = [];
const failed: Array<{ engine: string; error: string }> = [];
for (const [engine, url] of Object.entries(urls)) {
try {
const response = await fetch(url, { method: 'GET' });
if (response.ok) {
success.push(engine);
} else {
failed.push({
engine,
error: `HTTP ${response.status}: ${response.statusText}`,
});
}
} catch (error) {
failed.push({
engine,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}
return { success, failed };
}
}
/**
*
*/
export function generateSitemapIndex(baseUrl: string): string {
const lastmod = new Date().toISOString();
return `<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>${baseUrl}/sitemap.xml</loc>
<lastmod>${lastmod}</lastmod>
</sitemap>
</sitemapindex>`;
}
/**
* HTML
*/
export function generateHtmlSitemap(baseUrl: string): string {
const sitemap = generateSitemap(baseUrl);
const stats = getSitemapStats(baseUrl);
// 按页面类型分组
const groupedPages = sitemap.reduce(
(acc, entry) => {
let category = 'other';
if (entry.url.includes('/blog/') && !entry.url.endsWith('/blog')) {
category = 'blog-posts';
} else if (entry.url.endsWith('/blog')) {
category = 'blog';
} else if (entry.url.endsWith('/contact')) {
category = 'contact';
} else if (entry.url.endsWith('/products')) {
category = 'products';
} else if (entry.url === baseUrl || entry.url.match(/\/[a-z-]+$/)) {
category = 'main';
}
if (!acc[category]) acc[category] = [];
acc[category].push(entry);
return acc;
},
{} as Record<string, typeof sitemap>,
);
const categoryNames = {
main: '主要页面',
blog: '博客',
'blog-posts': '博客文章',
contact: '联系我们',
products: '产品',
other: '其他页面',
};
let html = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> - Eco Life</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 40px; line-height: 1.6; }
.header { border-bottom: 2px solid #10b981; padding-bottom: 20px; margin-bottom: 30px; }
.stats { background: #f0fdf4; padding: 20px; border-radius: 8px; margin-bottom: 30px; }
.category { margin-bottom: 30px; }
.category h2 { color: #10b981; border-bottom: 1px solid #d1d5db; padding-bottom: 10px; }
.url-list { list-style: none; padding: 0; }
.url-item { margin: 8px 0; padding: 12px; background: #f9fafb; border-radius: 6px; }
.url-item a { text-decoration: none; color: #1f2937; font-weight: 500; }
.url-item a:hover { color: #10b981; }
.url-meta { font-size: 0.875rem; color: #6b7280; margin-top: 4px; }
.priority { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 0.75rem; }
.priority-high { background: #fef3c7; color: #92400e; }
.priority-medium { background: #dbeafe; color: #1e40af; }
.priority-low { background: #f3f4f6; color: #374151; }
</style>
</head>
<body>
<div class="header">
<h1></h1>
<p>生成时间: ${new Date().toLocaleString('zh-CN')}</p>
</div>
<div class="stats">
<h3></h3>
<p><strong>:</strong> ${stats.urlCount}</p>
<p><strong>:</strong> ${stats.staticPages}</p>
<p><strong>:</strong> ${stats.blogPages}</p>
<p><strong>:</strong> ${stats.languages} (${locales.join(', ')})</p>
</div>`;
Object.entries(groupedPages).forEach(([category, pages]) => {
html += `
<div class="category">
<h2>${categoryNames[category as keyof typeof categoryNames] || category}</h2>
<ul class="url-list">`;
pages.forEach((page) => {
const priorityClass =
page.priority >= 0.8
? 'priority-high'
: page.priority >= 0.6
? 'priority-medium'
: 'priority-low';
html += `
<li class="url-item">
<a href="${page.url}" target="_blank">${page.url}</a>
<div class="url-meta">
<span class="priority ${priorityClass}">优先级: ${page.priority}</span>
<span>更新频率: ${page.changeFrequency}</span>
<span>最后修改: ${page.lastModified.toLocaleDateString('zh-CN')}</span>
</div>
</li>`;
});
html += `
</ul>
</div>`;
});
html += `
</body>
</html>`;
return html;
}