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 react from "@vitejs/plugin-react-swc";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { componentTagger } from "lovable-tagger";
|
import { componentTagger } from "lovable-tagger";
|
||||||
|
import { rssPlugin } from "./scripts/rss";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
|
|
@ -12,7 +13,15 @@ export default defineConfig(({ mode }) => ({
|
||||||
overlay: false,
|
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: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue