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);
|
await staticBlogGenerator.generate(posts);
|
||||||
console.log('✓ Generated static blog pages');
|
console.log('✓ Generated static blog pages');
|
||||||
|
await generateRssFeed(posts, buildDir);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code === 'ENOENT') {
|
if (err.code === 'ENOENT') {
|
||||||
console.log('⚠ No blog directory found, skipping blog processing');
|
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
|
// Run the build
|
||||||
async function build() {
|
async function build() {
|
||||||
const buildDir = path.join(__dirname, 'build');
|
const buildDir = path.join(__dirname, 'build');
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ class StaticBlogGenerator {
|
|||||||
` <meta name="description" content="${description}">`,
|
` <meta name="description" content="${description}">`,
|
||||||
` <link rel="canonical" href="${canonicalUrl}">`,
|
` <link rel="canonical" href="${canonicalUrl}">`,
|
||||||
...ogMeta,
|
...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="32x32" href="/logos/32px.png">',
|
||||||
' <link rel="icon" type="image/png" sizes="64x64" href="/logos/64px.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">',
|
' <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">',
|
'<body class="blog-page">',
|
||||||
'<nav class="quick-links" aria-label="Primary">',
|
'<nav class="quick-links" aria-label="Primary">',
|
||||||
this.renderHeaderLinks(),
|
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>',
|
'</nav>',
|
||||||
this.renderScrollTopBar(),
|
this.renderScrollTopBar(),
|
||||||
bodyHtml,
|
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);";
|
metaDiv.style.cssText = "margin-bottom: 1em; padding-bottom: 0.5em; border-bottom: 1px solid rgba(255,255,255,0.1);";
|
||||||
|
|
||||||
if (post.title) {
|
if (post.title) {
|
||||||
const titleEl = document.createElement("h2");
|
const titleEl = document.createElement("a");
|
||||||
|
titleEl.href = `/blog/${encodeURIComponent(post.slug)}/`;
|
||||||
titleEl.textContent = post.title;
|
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);
|
metaDiv.appendChild(titleEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,9 +74,12 @@ export class BlogPostNode extends NodeBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (metaInfo.length > 0) {
|
if (metaInfo.length > 0) {
|
||||||
const infoEl = document.createElement("div");
|
const infoEl = document.createElement("a");
|
||||||
infoEl.style.cssText = "color: rgba(255,255,255,0.6); font-size: 0.9em;";
|
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.textContent = metaInfo.join(' • ');
|
||||||
|
infoEl.addEventListener('mouseenter', () => infoEl.style.textDecoration = 'underline');
|
||||||
|
infoEl.addEventListener('mouseleave', () => infoEl.style.textDecoration = 'none');
|
||||||
metaDiv.appendChild(infoEl);
|
metaDiv.appendChild(infoEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -586,6 +586,20 @@ svg.world-image {
|
|||||||
color: rgba(205, 214, 244, 0.9);
|
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 ─────────────────────────────────────────────────────── */
|
/* ─── Static Blog Pages ─────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
body.blog-page {
|
body.blog-page {
|
||||||
|
|||||||
Reference in New Issue
Block a user