Co-authored-by: Damaj301damaj-lol <90093996+Damaj301damaj-lol@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot] 2026-05-19 15:29:27 +00:00
parent 1a9cf64271
commit ebcca7fc4e
2 changed files with 130 additions and 1 deletions

120
scripts/rss.ts Normal file
View file

@ -0,0 +1,120 @@
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<string, any>; 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<string, any> = {};
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
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 ` <item>
<title>${esc(p.title)}</title>
<link>${esc(url)}</link>
<guid isPermaLink="true">${esc(url)}</guid>
<pubDate>${new Date(p.date).toUTCString()}</pubDate>${p.description ? `\n <description>${esc(p.description)}</description>` : ''}
</item>`;
})
.join('\n');
return `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>${esc(opts.title)}</title>
<link>${esc(site)}</link>
<description>${esc(opts.description)}</description>
<atom:link href="${esc(site)}/rss.xml" rel="self" type="application/rss+xml" />
<language>en-us</language>
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
${items}
</channel>
</rss>`;
}
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());
},
};
}

View file

@ -2,6 +2,7 @@ import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import path from "path";
import { componentTagger } from "lovable-tagger";
import { rssPlugin } from "./scripts/rss";
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
@ -12,7 +13,15 @@ export default defineConfig(({ mode }) => ({
overlay: false,
},
},
plugins: [react(), mode === "development" && componentTagger()].filter(Boolean),
plugins: [
react(),
mode === "development" && componentTagger(),
rssPlugin({
siteUrl: "https://damaj.tech",
title: "Damaj — Blog",
description: "Writing on engineering, systems, and craft.",
}),
].filter(Boolean),
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),