|
| 1 | +import type { Plugin } from 'vite' |
| 2 | +import fs from 'fs' |
| 3 | +import path from 'path' |
| 4 | +import { execSync } from 'child_process' |
| 5 | + |
| 6 | +const BASE_URL = 'https://webfiori.com' |
| 7 | + |
| 8 | +const STATIC_ROUTES = [ |
| 9 | + { path: '/', changefreq: 'weekly', priority: '1.0' }, |
| 10 | + { path: '/v3', changefreq: 'monthly', priority: '0.8' }, |
| 11 | + { path: '/features', changefreq: 'monthly', priority: '0.8' }, |
| 12 | + { path: '/getting-started', changefreq: 'monthly', priority: '0.8' }, |
| 13 | + { path: '/docs', changefreq: 'weekly', priority: '0.9' }, |
| 14 | + { path: '/blog', changefreq: 'weekly', priority: '0.9' }, |
| 15 | + { path: '/libraries', changefreq: 'monthly', priority: '0.6' }, |
| 16 | + { path: '/contributing', changefreq: 'monthly', priority: '0.5' }, |
| 17 | +] |
| 18 | + |
| 19 | +function getLastMod(filePath: string): string { |
| 20 | + try { |
| 21 | + const out = execSync(`git log -1 --format=%cI -- "${filePath}"`, { |
| 22 | + cwd: path.dirname(filePath), |
| 23 | + encoding: 'utf-8', |
| 24 | + }).trim() |
| 25 | + if (out) return out.slice(0, 10) |
| 26 | + } catch { /* ignore */ } |
| 27 | + try { |
| 28 | + return fs.statSync(filePath).mtime.toISOString().slice(0, 10) |
| 29 | + } catch { |
| 30 | + return new Date().toISOString().slice(0, 10) |
| 31 | + } |
| 32 | +} |
| 33 | + |
| 34 | +function parseFrontmatterDate(raw: string): string { |
| 35 | + const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/) |
| 36 | + if (!match) return '' |
| 37 | + const dateMatch = match[1].match(/^date:\s*(.+)$/m) |
| 38 | + return dateMatch ? dateMatch[1].trim().replace(/["']/g, '') : '' |
| 39 | +} |
| 40 | + |
| 41 | +function buildUrl(entry: { loc: string; lastmod?: string; changefreq?: string; priority?: string }): string { |
| 42 | + let xml = ` <url>\n <loc>${entry.loc}</loc>\n` |
| 43 | + if (entry.lastmod) xml += ` <lastmod>${entry.lastmod}</lastmod>\n` |
| 44 | + if (entry.changefreq) xml += ` <changefreq>${entry.changefreq}</changefreq>\n` |
| 45 | + if (entry.priority) xml += ` <priority>${entry.priority}</priority>\n` |
| 46 | + xml += ` </url>` |
| 47 | + return xml |
| 48 | +} |
| 49 | + |
| 50 | +export default function sitemapPlugin(docsDir: string, blogDir: string): Plugin { |
| 51 | + return { |
| 52 | + name: 'vite-plugin-sitemap', |
| 53 | + apply: 'build', |
| 54 | + closeBundle() { |
| 55 | + const today = new Date().toISOString().slice(0, 10) |
| 56 | + const urls: string[] = [] |
| 57 | + |
| 58 | + // Static routes |
| 59 | + for (const route of STATIC_ROUTES) { |
| 60 | + urls.push(buildUrl({ |
| 61 | + loc: `${BASE_URL}${route.path}`, |
| 62 | + lastmod: today, |
| 63 | + changefreq: route.changefreq, |
| 64 | + priority: route.priority, |
| 65 | + })) |
| 66 | + } |
| 67 | + |
| 68 | + // Doc pages |
| 69 | + const docsResolved = path.resolve(docsDir) |
| 70 | + if (fs.existsSync(docsResolved)) { |
| 71 | + for (const file of fs.readdirSync(docsResolved)) { |
| 72 | + if (!file.endsWith('.md') || file === 'README.md' || file === 'index.md') continue |
| 73 | + const slug = file.replace('.md', '') |
| 74 | + const filePath = path.join(docsResolved, file) |
| 75 | + urls.push(buildUrl({ |
| 76 | + loc: `${BASE_URL}/docs/${slug}`, |
| 77 | + lastmod: getLastMod(filePath), |
| 78 | + changefreq: 'monthly', |
| 79 | + priority: '0.7', |
| 80 | + })) |
| 81 | + } |
| 82 | + } |
| 83 | + |
| 84 | + // Blog posts (only published) |
| 85 | + const blogResolved = path.resolve(blogDir) |
| 86 | + if (fs.existsSync(blogResolved)) { |
| 87 | + for (const file of fs.readdirSync(blogResolved)) { |
| 88 | + if (!file.endsWith('.md')) continue |
| 89 | + const filePath = path.join(blogResolved, file) |
| 90 | + const raw = fs.readFileSync(filePath, 'utf-8') |
| 91 | + const postDate = parseFrontmatterDate(raw) |
| 92 | + |
| 93 | + // Exclude future posts |
| 94 | + if (postDate && postDate > today) continue |
| 95 | + |
| 96 | + const slug = file.replace('.md', '') |
| 97 | + urls.push(buildUrl({ |
| 98 | + loc: `${BASE_URL}/blog/${slug}`, |
| 99 | + lastmod: postDate || getLastMod(filePath), |
| 100 | + changefreq: 'yearly', |
| 101 | + priority: '0.7', |
| 102 | + })) |
| 103 | + } |
| 104 | + } |
| 105 | + |
| 106 | + const sitemap = `<?xml version="1.0" encoding="UTF-8"?> |
| 107 | +<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> |
| 108 | +${urls.join('\n')} |
| 109 | +</urlset> |
| 110 | +` |
| 111 | + |
| 112 | + const outDir = path.resolve('dist') |
| 113 | + fs.mkdirSync(outDir, { recursive: true }) |
| 114 | + fs.writeFileSync(path.join(outDir, 'sitemap.xml'), sitemap) |
| 115 | + console.log(`✓ sitemap.xml generated with ${urls.length} URLs`) |
| 116 | + }, |
| 117 | + } |
| 118 | +} |
0 commit comments