From ebcca7fc4e919a1f02daec2e54a44209a2e8efc2 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 15:29:27 +0000 Subject: [PATCH 1/3] Changes Co-authored-by: Damaj301damaj-lol <90093996+Damaj301damaj-lol@users.noreply.github.com> --- scripts/rss.ts | 120 +++++++++++++++++++++++++++++++++++++++++++++++++ vite.config.ts | 11 ++++- 2 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 scripts/rss.ts diff --git a/scripts/rss.ts b/scripts/rss.ts new file mode 100644 index 0000000..fd93476 --- /dev/null +++ b/scripts/rss.ts @@ -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; 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()); + }, + }; +} diff --git a/vite.config.ts b/vite.config.ts index a9b992f..b6cb8fa 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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"), From 57c8096816f5e7b473a9a0bb51fd4d2c555fbfad Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 15:29:47 +0000 Subject: [PATCH 2/3] Changes Co-authored-by: Damaj301damaj-lol <90093996+Damaj301damaj-lol@users.noreply.github.com> --- index.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.html b/index.html index 38a5fa7..0e2d4af 100644 --- a/index.html +++ b/index.html @@ -17,6 +17,8 @@ + + From 0ec930bf95af78e5497ccbb6603a7c2fb31bfd87 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 15:29:55 +0000 Subject: [PATCH 3/3] Changes Co-authored-by: Damaj301damaj-lol <90093996+Damaj301damaj-lol@users.noreply.github.com> --- src/pages/Blog.tsx | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/pages/Blog.tsx b/src/pages/Blog.tsx index 45936a7..01ee96d 100644 --- a/src/pages/Blog.tsx +++ b/src/pages/Blog.tsx @@ -39,11 +39,22 @@ const Blog = () => {
-
- -

- $ blog -

+
+
+ +

+ $ blog +

+
+ + + RSS +

Notes on self-hosting, linux, and whatever else.