125 lines
3.9 KiB
JavaScript
125 lines
3.9 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
const PUBLIC_DIR = path.join(ROOT_DIR, 'public');
|
|
const SKILLS_JSON = path.join(PUBLIC_DIR, 'skills.json');
|
|
const OUTPUT_PATH = path.join(PUBLIC_DIR, 'sitemap.xml');
|
|
const BASE_PATH =
|
|
(process.env.VITE_BASE_PATH || '/').trim().replace(/\/+$/, '');
|
|
const NORMALIZED_BASE_PATH = BASE_PATH && BASE_PATH !== '/' ? BASE_PATH : '';
|
|
const DEFAULT_SITE_URL = `http://localhost${NORMALIZED_BASE_PATH}`;
|
|
|
|
const SITE_URL = (process.env.SEO_SITE_URL || process.env.WEBSITE_BASE_URL || DEFAULT_SITE_URL).replace(/\/$/, '');
|
|
const TOP_SKILL_COUNT = Number.parseInt(process.env.TOP_SKILL_COUNT || '40', 10);
|
|
const DEFAULT_LASTMOD = new Date().toISOString().slice(0, 10);
|
|
|
|
function getTopSkillCount() {
|
|
return Number.isFinite(TOP_SKILL_COUNT) ? Math.max(TOP_SKILL_COUNT, 0) : 40;
|
|
}
|
|
|
|
function escapeXml(text) {
|
|
return String(text)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
export function getDateScore(dateValue) {
|
|
if (!dateValue) return 0;
|
|
|
|
const parsed = Date.parse(dateValue);
|
|
return Number.isNaN(parsed) ? 0 : parsed;
|
|
}
|
|
|
|
function normalizeSkillId(skillId) {
|
|
return encodeURIComponent(String(skillId).trim());
|
|
}
|
|
|
|
export function selectTopSkillEntries(skills, topCount = TOP_SKILL_COUNT) {
|
|
const max = Math.max(Number.parseInt(topCount, 10) || 0, 0);
|
|
if (!Array.isArray(skills) || max === 0) {
|
|
return [];
|
|
}
|
|
|
|
const sorted = [...skills]
|
|
.map((skill, index) => ({
|
|
id: skill.id,
|
|
index,
|
|
stars: Number(skill?.stars) || 0,
|
|
date: getDateScore(skill?.date_added),
|
|
}))
|
|
.filter((item) => Boolean(item.id))
|
|
.sort((a, b) => {
|
|
if (a.stars !== b.stars) return b.stars - a.stars;
|
|
if (a.date !== b.date) return b.date - a.date;
|
|
|
|
const nameCompare = String(a.id).localeCompare(String(b.id), undefined, { sensitivity: 'base' });
|
|
if (nameCompare !== 0) return nameCompare;
|
|
|
|
return a.index - b.index;
|
|
})
|
|
.slice(0, max);
|
|
|
|
const dedupedEntries = [];
|
|
const seen = new Set();
|
|
|
|
for (const item of sorted) {
|
|
if (!item.id || seen.has(item.id)) {
|
|
continue;
|
|
}
|
|
seen.add(item.id);
|
|
dedupedEntries.push(`/skill/${normalizeSkillId(item.id)}`);
|
|
if (dedupedEntries.length >= max) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return dedupedEntries;
|
|
}
|
|
|
|
export function generateSitemapXml({ baseUrl, paths, lastmod = DEFAULT_LASTMOD }) {
|
|
const normalizedBase = String(baseUrl).replace(/\/$/, '');
|
|
const uniquePaths = [...new Set(paths)];
|
|
|
|
const urlsXml = uniquePaths
|
|
.map((pathName) => {
|
|
const href = `${normalizedBase}${pathName}`;
|
|
return ` <url>\n <loc>${escapeXml(href)}</loc>\n <lastmod>${lastmod}</lastmod>\n <changefreq>${pathName === '/' ? 'daily' : 'weekly'}</changefreq>\n <priority>${pathName === '/' ? '1.0' : '0.7'}</priority>\n </url>`;
|
|
})
|
|
.join('\n');
|
|
|
|
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9">\n${urlsXml}\n</urlset>\n`;
|
|
}
|
|
|
|
function readSkillsCatalog() {
|
|
if (!fs.existsSync(SKILLS_JSON)) {
|
|
throw new Error(`Skills catalog not found at ${SKILLS_JSON}`);
|
|
}
|
|
|
|
const raw = fs.readFileSync(SKILLS_JSON, 'utf-8');
|
|
return JSON.parse(raw);
|
|
}
|
|
|
|
export function buildSitemap(skills, topCount = TOP_SKILL_COUNT, baseUrl = SITE_URL) {
|
|
const topSkillPaths = selectTopSkillEntries(skills, topCount);
|
|
return generateSitemapXml({
|
|
baseUrl,
|
|
paths: ['/', ...topSkillPaths],
|
|
});
|
|
}
|
|
|
|
function writeSitemap() {
|
|
const skills = readSkillsCatalog();
|
|
const xml = buildSitemap(skills, getTopSkillCount(), SITE_URL);
|
|
fs.writeFileSync(OUTPUT_PATH, xml, 'utf-8');
|
|
console.log(`sitemap.xml generated at ${OUTPUT_PATH}`);
|
|
}
|
|
|
|
if (process.argv[1] && process.argv[1].endsWith('generate-sitemap.js')) {
|
|
writeSitemap();
|
|
}
|