mirror of
https://github.com/litruv/lit.ruv.wtf.git
synced 2026-07-24 02:36:02 +10:00
feat: add RSS feed generation and integrate RSS link in blog pages
This commit is contained in:
75
build.js
75
build.js
@@ -96,6 +96,7 @@ async function processBlogPosts(buildDir) {
|
||||
|
||||
await staticBlogGenerator.generate(posts);
|
||||
console.log('✓ Generated static blog pages');
|
||||
await generateRssFeed(posts, buildDir);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.log('⚠ No blog directory found, skipping blog processing');
|
||||
@@ -362,6 +363,80 @@ async function updateHtmlForBundle() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an RSS 2.0 feed from blog posts and writes it to build/rss.xml.
|
||||
*
|
||||
* @param {Array<{slug: string, title: string, date: string|null, author: string|null, content: string}>} posts
|
||||
* @param {string} buildDir
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function generateRssFeed(posts, buildDir) {
|
||||
const siteUrl = 'https://lit.ruv.wtf';
|
||||
const feedUrl = `${siteUrl}/rss.xml`;
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
function xmlEscape(str) {
|
||||
return (str || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips markdown syntax to produce plain-text for descriptions.
|
||||
*
|
||||
* @param {string} md
|
||||
* @returns {string}
|
||||
*/
|
||||
function mdToPlain(md) {
|
||||
return (md || '')
|
||||
.replace(/```[\s\S]*?```/g, '')
|
||||
.replace(/`[^`]+`/g, '')
|
||||
.replace(/!\[.*?\]\(.*?\)/g, '')
|
||||
.replace(/\[([^\]]+)\]\(.*?\)/g, '$1')
|
||||
.replace(/^#{1,6}\s+/gm, '')
|
||||
.replace(/[*_]{1,3}([^*_]+)[*_]{1,3}/g, '$1')
|
||||
.replace(/^[-*_]{3,}$/gm, '')
|
||||
.replace(/\n{2,}/g, ' ')
|
||||
.replace(/\n/g, ' ')
|
||||
.trim()
|
||||
.slice(0, 400);
|
||||
}
|
||||
|
||||
const items = posts.map(post => {
|
||||
const url = `${siteUrl}/blog/${encodeURIComponent(post.slug)}/`;
|
||||
const pubDate = post.date ? new Date(post.date).toUTCString() : '';
|
||||
const description = xmlEscape(mdToPlain(post.content));
|
||||
return [
|
||||
' <item>',
|
||||
` <title>${xmlEscape(post.title || post.slug)}</title>`,
|
||||
` <link>${url}</link>`,
|
||||
` <guid isPermaLink="true">${url}</guid>`,
|
||||
pubDate ? ` <pubDate>${pubDate}</pubDate>` : '',
|
||||
post.author ? ` <author>${xmlEscape(post.author)}</author>` : '',
|
||||
` <description>${description}</description>`,
|
||||
' </item>',
|
||||
].filter(Boolean).join('\n');
|
||||
});
|
||||
|
||||
const xml = [
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
'<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">',
|
||||
' <channel>',
|
||||
' <title>lit.ruv.wtf</title>',
|
||||
` <link>${siteUrl}</link>`,
|
||||
' <description>Blog posts from lit.ruv.wtf</description>',
|
||||
' <language>en-us</language>',
|
||||
` <atom:link href="${feedUrl}" rel="self" type="application/rss+xml" />`,
|
||||
...items,
|
||||
' </channel>',
|
||||
'</rss>',
|
||||
].join('\n');
|
||||
|
||||
await fs.writeFile(path.join(buildDir, 'rss.xml'), xml, 'utf-8');
|
||||
console.log('✓ Generated RSS feed');
|
||||
}
|
||||
|
||||
// Run the build
|
||||
async function build() {
|
||||
const buildDir = path.join(__dirname, 'build');
|
||||
|
||||
@@ -229,6 +229,7 @@ class StaticBlogGenerator {
|
||||
` <meta name="description" content="${description}">`,
|
||||
` <link rel="canonical" href="${canonicalUrl}">`,
|
||||
...ogMeta,
|
||||
` <link rel="alternate" type="application/rss+xml" title="lit.ruv.wtf RSS" href="/rss.xml">`,
|
||||
' <link rel="icon" type="image/png" sizes="32x32" href="/logos/32px.png">',
|
||||
' <link rel="icon" type="image/png" sizes="64x64" href="/logos/64px.png">',
|
||||
' <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">',
|
||||
@@ -245,6 +246,10 @@ class StaticBlogGenerator {
|
||||
'<body class="blog-page">',
|
||||
'<nav class="quick-links" aria-label="Primary">',
|
||||
this.renderHeaderLinks(),
|
||||
' <a class="quick-link rss-link" href="/rss.xml" aria-label="RSS feed" title="RSS feed">',
|
||||
' <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6.18 15.64a2.18 2.18 0 0 1 2.18 2.18C8.36 19.01 7.38 20 6.18 20C4.98 20 4 19.01 4 17.82a2.18 2.18 0 0 1 2.18-2.18M4 4.44A15.56 15.56 0 0 1 19.56 20h-2.83A12.73 12.73 0 0 0 4 7.27V4.44m0 5.66a9.9 9.9 0 0 1 9.9 9.9h-2.83A7.07 7.07 0 0 0 4 12.93V10.1z"/></svg>',
|
||||
' RSS',
|
||||
' </a>',
|
||||
'</nav>',
|
||||
this.renderScrollTopBar(),
|
||||
bodyHtml,
|
||||
|
||||
@@ -55,9 +55,12 @@ export class BlogPostNode extends NodeBase {
|
||||
metaDiv.style.cssText = "margin-bottom: 1em; padding-bottom: 0.5em; border-bottom: 1px solid rgba(255,255,255,0.1);";
|
||||
|
||||
if (post.title) {
|
||||
const titleEl = document.createElement("h2");
|
||||
const titleEl = document.createElement("a");
|
||||
titleEl.href = `/blog/${encodeURIComponent(post.slug)}/`;
|
||||
titleEl.textContent = post.title;
|
||||
titleEl.style.cssText = "margin: 0 0 0.5em 0; font-size: 1.5em;";
|
||||
titleEl.style.cssText = "display:block; margin: 0 0 0.5em 0; font-size: 1.5em; color: inherit; text-decoration: none;";
|
||||
titleEl.addEventListener('mouseenter', () => titleEl.style.textDecoration = 'underline');
|
||||
titleEl.addEventListener('mouseleave', () => titleEl.style.textDecoration = 'none');
|
||||
metaDiv.appendChild(titleEl);
|
||||
}
|
||||
|
||||
@@ -71,9 +74,12 @@ export class BlogPostNode extends NodeBase {
|
||||
}
|
||||
|
||||
if (metaInfo.length > 0) {
|
||||
const infoEl = document.createElement("div");
|
||||
infoEl.style.cssText = "color: rgba(255,255,255,0.6); font-size: 0.9em;";
|
||||
const infoEl = document.createElement("a");
|
||||
infoEl.href = `/blog/${encodeURIComponent(post.slug)}/`;
|
||||
infoEl.style.cssText = "display:block; color: rgba(255,255,255,0.6); font-size: 0.9em; text-decoration: none;";
|
||||
infoEl.textContent = metaInfo.join(' • ');
|
||||
infoEl.addEventListener('mouseenter', () => infoEl.style.textDecoration = 'underline');
|
||||
infoEl.addEventListener('mouseleave', () => infoEl.style.textDecoration = 'none');
|
||||
metaDiv.appendChild(infoEl);
|
||||
}
|
||||
|
||||
|
||||
@@ -586,6 +586,20 @@ svg.world-image {
|
||||
color: rgba(205, 214, 244, 0.9);
|
||||
}
|
||||
|
||||
.rss-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: #f09040;
|
||||
opacity: 0.7;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.rss-link:hover {
|
||||
color: #f09040;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ─── Static Blog Pages ─────────────────────────────────────────────────────── */
|
||||
|
||||
body.blog-page {
|
||||
|
||||
Reference in New Issue
Block a user