import fs from 'fs'; import path from 'path'; import { execFileSync } from 'child_process'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const DIST_DIR = path.resolve(__dirname, '../dist'); const LOCALES_DIR = path.resolve(__dirname, '../public/locales'); const SITE_URL = (process.env.SITE_URL || 'https://www.bentopdf.com').replace( /\/+$/, '' ); const EXCLUDED_PAGES = new Set(['404', 'wasm-settings']); const languages = fs.readdirSync(LOCALES_DIR).filter((file) => { return fs.statSync(path.join(LOCALES_DIR, file)).isDirectory(); }); const toCamelCase = (str) => { return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); }; const toolsByLang = {}; for (const lang of languages) { const toolsPath = path.join(LOCALES_DIR, lang, 'tools.json'); toolsByLang[lang] = fs.existsSync(toolsPath) ? JSON.parse(fs.readFileSync(toolsPath, 'utf-8')) : {}; } function alternateLangsFor(pageName) { if (pageName === 'index') return languages; const translationKey = toCamelCase(pageName); return languages.filter( (l) => l === 'en' || Boolean(toolsByLang[l][translationKey]) ); } const PRIORITY_MAP = { index: 1.0, tools: 0.9, 'pdf-converter': 0.9, 'pdf-editor': 0.9, 'pdf-security': 0.9, 'pdf-merge-split': 0.9, 'merge-pdf': 0.9, 'split-pdf': 0.9, 'compress-pdf': 0.9, 'edit-pdf': 0.9, 'word-to-pdf': 0.9, 'excel-to-pdf': 0.9, 'powerpoint-to-pdf': 0.9, 'jpg-to-pdf': 0.9, 'pdf-to-word': 0.9, 'unlock-pdf': 0.9, 'protect-pdf': 0.9, 'pdf-to-excel': 0.9, 'pdf-to-jpg': 0.9, 'compress-pdf-to-100kb': 0.8, 'compress-pdf-to-200kb': 0.8, 'compress-pdf-to-500kb': 0.8, 'compress-pdf-to-1mb': 0.8, 'compress-pdf-to-2mb': 0.8, 'compress-pdf-for-email': 0.8, about: 0.8, faq: 0.8, contact: 0.7, privacy: 0.5, terms: 0.5, licensing: 0.5, }; function getPriority(pageName) { return PRIORITY_MAP[pageName] || 0.7; } function buildUrl(lang, pageName) { const pagePath = pageName === 'index' ? '' : pageName; if (lang === 'en') { return pagePath ? `${SITE_URL}/${pagePath}` : SITE_URL; } return pagePath ? `${SITE_URL}/${lang}/${pagePath}` : `${SITE_URL}/${lang}/`; } function generateSitemap() { console.log('🗺️ Generating multilingual sitemap...'); console.log(` SITE_URL: ${SITE_URL}`); console.log(` Languages: ${languages.join(', ')}`); const htmlFiles = fs .readdirSync(DIST_DIR) .filter((file) => file.endsWith('.html')) .map((file) => file.replace('.html', '')) .filter((name) => !EXCLUDED_PAGES.has(name)); const projectRoot = path.resolve(__dirname, '..'); const lastModCache = new Map(); const getLastMod = (pageName) => { if (lastModCache.has(pageName)) return lastModCache.get(pageName); const fileName = `${pageName}.html`; const candidates = [ path.join(projectRoot, 'src', 'pages', fileName), path.join(projectRoot, fileName), ]; let iso = null; for (const sourcePath of candidates) { if (!fs.existsSync(sourcePath)) continue; try { const out = execFileSync( 'git', ['log', '-1', '--format=%cI', '--', sourcePath], { cwd: projectRoot, encoding: 'utf-8' } ).trim(); if (out) iso = out.slice(0, 10); } catch { iso = null; } if (!iso) { try { iso = fs.statSync(sourcePath).mtime.toISOString().slice(0, 10); } catch { iso = null; } } break; } if (!iso) { try { iso = fs .statSync(path.join(DIST_DIR, fileName)) .mtime.toISOString() .slice(0, 10); } catch { iso = new Date().toISOString().slice(0, 10); } } lastModCache.set(pageName, iso); return iso; }; let sitemap = ` `; for (const pageName of htmlFiles) { const priority = getPriority(pageName); const url = buildUrl('en', pageName); const lastmod = getLastMod(pageName); sitemap += ` ${url} ${lastmod} ${priority} `; for (const altLang of alternateLangsFor(pageName)) { const altUrl = buildUrl(altLang, pageName); sitemap += ` `; } const defaultUrl = buildUrl('en', pageName); sitemap += ` `; } const blogDir = path.join(DIST_DIR, 'blog'); let blogCount = 0; if (fs.existsSync(blogDir)) { const blogFiles = fs .readdirSync(blogDir) .filter((file) => file.endsWith('.html')) .map((file) => file.replace('.html', '')); for (const name of blogFiles) { const url = name === 'index' ? `${SITE_URL}/blog/` : `${SITE_URL}/blog/${name}`; const sourcePath = path.join(projectRoot, 'blog', `${name}.html`); let lastmod; try { const out = execFileSync( 'git', ['log', '-1', '--format=%cI', '--', sourcePath], { cwd: projectRoot, encoding: 'utf-8' } ).trim(); lastmod = out ? out.slice(0, 10) : null; } catch { lastmod = null; } if (!lastmod) { try { lastmod = fs.statSync(sourcePath).mtime.toISOString().slice(0, 10); } catch { lastmod = new Date().toISOString().slice(0, 10); } } sitemap += ` ${url} ${lastmod} 0.7 `; blogCount++; } } if (blogCount > 0) { console.log(` Blog: ${blogCount} URLs added (English only)`); } sitemap += ` `; const sitemapPath = path.join(DIST_DIR, 'sitemap.xml'); fs.writeFileSync(sitemapPath, sitemap); const publicSitemapPath = path.resolve(__dirname, '../public/sitemap.xml'); fs.writeFileSync(publicSitemapPath, sitemap); console.log( `✅ Sitemap generated with ${htmlFiles.length} canonical URLs (${languages.length} hreflang alternates each)` ); } generateSitemap();