diff --git a/frontend/package.json b/frontend/package.json index a013e26..2a9b918 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,7 +8,7 @@ "generate:metadata": "cd .. && npx tsx scripts/generate-component-metadata.ts", "generate:favicons": "node ../scripts/generate-favicons.mjs", "generate:og-image": "node ../scripts/generate-og-image.mjs", - "generate:sitemap": "npx tsx scripts/generate-sitemap.ts", + "generate:sitemap": "node scripts/generate-sitemap.mjs", "dev": "npm run generate:metadata && vite", "build": "npm run generate:metadata && npm run generate:sitemap && tsc -b && vite build", "build:docker": "npm run generate:sitemap && vite build", diff --git a/frontend/scripts/generate-sitemap.mjs b/frontend/scripts/generate-sitemap.mjs new file mode 100644 index 0000000..27ae1a9 --- /dev/null +++ b/frontend/scripts/generate-sitemap.mjs @@ -0,0 +1,77 @@ +/** + * Auto-generates public/sitemap.xml by parsing seoRoutes.ts + * Pure Node.js — no tsx/ts-node needed. + * Run: node scripts/generate-sitemap.mjs [--ping] + */ + +import { readFileSync, writeFileSync } from 'fs'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const DOMAIN = 'https://velxio.dev'; +const TODAY = new Date().toISOString().slice(0, 10); + +// Parse seoRoutes.ts to extract the route objects +const seoRoutesPath = resolve(__dirname, '../src/seoRoutes.ts'); +const source = readFileSync(seoRoutesPath, 'utf-8'); + +// Extract the array content between SEO_ROUTES = [ ... ]; +const match = source.match(/SEO_ROUTES[^=]*=\s*\[([\s\S]*?)\];/); +if (!match) { + console.error('Could not parse SEO_ROUTES from seoRoutes.ts'); + process.exit(1); +} + +// Evaluate the array (safe: only contains string/number/boolean literals) +// Convert TS-style comments and trailing commas to valid JSON-ish +const arrayStr = match[1] + .replace(/\/\/.*$/gm, '') // remove line comments + .replace(/\/\*[\s\S]*?\*\//g, ''); // remove block comments + +// Use Function constructor to evaluate the JS array literal +const routes = new Function(`return [${arrayStr}]`)(); + +const indexable = routes.filter((r) => !r.noindex); + +const xml = ` + +${indexable + .map( + (r) => ` + + ${DOMAIN}${r.path} + ${TODAY} + ${r.changefreq ?? 'monthly'} + ${r.priority ?? 0.5} + ` + ) + .join('')} + + +`; + +const outPath = resolve(__dirname, '../public/sitemap.xml'); +writeFileSync(outPath, xml.trimStart(), 'utf-8'); +console.log(`sitemap.xml generated → ${indexable.length} URLs (${TODAY})`); + +// Ping search engines (optional) +if (process.argv.includes('--ping')) { + const sitemapUrl = `${DOMAIN}/sitemap.xml`; + const pings = [ + `https://www.google.com/ping?sitemap=${encodeURIComponent(sitemapUrl)}`, + `https://www.bing.com/ping?sitemap=${encodeURIComponent(sitemapUrl)}`, + ]; + console.log('Pinging search engines...'); + await Promise.allSettled( + pings.map(async (url) => { + try { + const res = await fetch(url); + console.log(` ${res.ok ? 'OK' : 'FAIL'} ${url.split('?')[0]}`); + } catch (e) { + console.log(` FAIL ${url.split('?')[0]}: ${e.message}`); + } + }) + ); +}