Changes
Co-authored-by: Damaj301damaj-lol <90093996+Damaj301damaj-lol@users.noreply.github.com>
This commit is contained in:
parent
1a9cf64271
commit
ebcca7fc4e
2 changed files with 130 additions and 1 deletions
120
scripts/rss.ts
Normal file
120
scripts/rss.ts
Normal 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, '&').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 ` <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());
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -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"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue