import { readFileSync, readdirSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'; import { join, resolve } from 'node:path'; export interface RssOptions { siteUrl: string; title: string; description: string; blogDir?: string; outFile?: string; } interface Meta { slug: string; title: string; date: string; description?: string; tags?: string[]; draft?: boolean; content: string; } function parseValue(value: string): string | string[] | boolean { const t = value.trim(); if (t === 'true') return true; if (t === 'false') return false; if (t.startsWith('[') && t.endsWith(']')) { return t.slice(1, -1).split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean); } return t.replace(/^['"]|['"]$/g, ''); } function parseFrontmatter(raw: string): { data: Record; content: string } { if (!raw.startsWith('---')) return { data: {}, content: raw }; const end = raw.indexOf('\n---', 3); if (end === -1) return { data: {}, content: raw }; const fm = raw.slice(3, end).trim(); const content = raw.slice(end + 4).replace(/^\s*\n/, ''); const data: Record = {}; for (const line of fm.split('\n')) { const sep = line.indexOf(':'); if (sep === -1) continue; data[line.slice(0, sep).trim()] = parseValue(line.slice(sep + 1)); } return { data, content }; } function esc(s: string): string { return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } export function loadPosts(blogDir: string): Meta[] { const dir = resolve(blogDir); if (!existsSync(dir)) return []; return readdirSync(dir) .filter((f) => f.endsWith('.md')) .map((file) => { const raw = readFileSync(join(dir, file), 'utf8'); const { data, content } = parseFrontmatter(raw); const slug = file.replace(/\.md$/, ''); return { slug, title: data.title ?? slug, date: data.date ? new Date(data.date).toISOString() : new Date().toISOString(), description: data.description, tags: data.tags ?? [], draft: data.draft ?? false, content, } as Meta; }) .filter((p) => !p.draft) .sort((a, b) => b.date.localeCompare(a.date)); } export function buildRss(opts: RssOptions): string { const blogDir = opts.blogDir ?? 'src/content/blog'; const posts = loadPosts(blogDir); const site = opts.siteUrl.replace(/\/$/, ''); const items = posts .map((p) => { const url = `${site}/blog/${p.slug}`; return ` ${esc(p.title)} ${esc(url)} ${esc(url)} ${new Date(p.date).toUTCString()}${p.description ? `\n ${esc(p.description)}` : ''} `; }) .join('\n'); return ` ${esc(opts.title)} ${esc(site)} ${esc(opts.description)} en-us ${new Date().toUTCString()} ${items} `; } export function rssPlugin(opts: RssOptions) { const generate = () => buildRss(opts); return { name: 'rss-feed', configureServer(server: any) { server.middlewares.use('/rss.xml', (_req: any, res: any) => { res.setHeader('Content-Type', 'application/rss+xml; charset=utf-8'); res.end(generate()); }); }, closeBundle() { const outDir = 'dist'; if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true }); writeFileSync(join(outDir, 'rss.xml'), generate()); }, }; }