first commit

This commit is contained in:
2026-01-25 11:33:37 -04:00
commit 6f42a58ee5
148 changed files with 32974 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
// Analytics tracking
(function() {
// Generate or retrieve visitor ID
function getVisitorId() {
let visitorId = localStorage.getItem('visitorId');
if (!visitorId) {
visitorId = 'visitor_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
localStorage.setItem('visitorId', visitorId);
}
return visitorId;
}
// Track page view
function trackPageView() {
const visitorId = getVisitorId();
const page = window.location.pathname + window.location.hash;
fetch('/api/track.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'pageview',
page: page,
visitorId: visitorId
})
}).catch(err => console.error('Analytics error:', err));
}
// Track page view on load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', trackPageView);
} else {
trackPageView();
}
// Track RSS link clicks
const rssLink = document.getElementById('rssLink');
if (rssLink) {
rssLink.addEventListener('click', function() {
const visitorId = getVisitorId();
fetch('/api/track.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'rss_click',
page: '/blog/feed.xml',
visitorId: visitorId
})
}).catch(err => console.error('RSS tracking error:', err));
});
}
})();
+61
View File
@@ -0,0 +1,61 @@
// Summon the blog posts from their JSON tomb
async function loadBlogPosts() {
try {
// Use relative path - this should work when blog.js is loaded from /blog/index.html
const apiPath = 'api/posts.php';
const response = await fetch(apiPath);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const posts = await response.json();
// Validate that we got an array
if (!Array.isArray(posts)) {
throw new Error('Invalid response format: expected array');
}
const container = document.getElementById("blogPostsContainer");
if (!container) {
throw new Error('Blog posts container not found');
}
// Newest first - we're not savages reading in chronological order
const sortedPosts = posts.sort((a, b) => new Date(b.date) - new Date(a.date));
// Out with the old (loading message probably)
container.innerHTML = '';
if (sortedPosts.length === 0) {
container.innerHTML = '<p class="error">No blog posts found. Add some posts to get started!</p>';
return;
}
sortedPosts.forEach(post => {
const postElement = document.createElement('div');
postElement.className = 'blog-post-preview';
postElement.id = `post-preview-${post.id}`;
postElement.innerHTML = `
<h2 class="blog-post-title"><a href="post.html?id=${post.id}" class="blog-post-link">${post.title}</a></h2>
<p class="blog-post-date">${post.date}</p>
<p class="blog-post-excerpt">${post.excerpt}</p>
<a href="post.html?id=${post.id}" class="blog-read-more">Read more →</a>
`;
container.appendChild(postElement);
});
} catch (error) {
console.error('Error loading blog posts:', error);
const container = document.getElementById("blogPostsContainer");
if (container) {
container.innerHTML =
'<p class="error">Error loading blog posts. Make sure you\'re running a PHP server and the posts.json file exists.</p>';
}
// Fail gracefully - no one likes a drama queen
}
}
// Start the blog parade when the page finishes loading
window.addEventListener('load', loadBlogPosts);
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env node
// Script to generate RSS feed from blog-posts.json
const fs = require('fs');
const path = require('path');
const postsPath = path.join(__dirname, 'blog-posts.json');
const feedPath = path.join(__dirname, 'feed.xml');
try {
const posts = JSON.parse(fs.readFileSync(postsPath, 'utf8'));
// Sort posts by date (newest first)
const sortedPosts = posts.sort((a, b) => new Date(b.date) - new Date(a.date));
// Escape XML special characters
const escapeXml = (str) => {
if (!str) return '';
return str.toString()
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
};
// Convert content newlines to HTML breaks
const formatContent = (content) => {
return content.split('\n\n')
.map(p => p.trim())
.filter(p => p.length > 0)
.join('<br/><br/>');
};
// Generate RSS items
const rssItems = sortedPosts.map(post => {
const title = escapeXml(post.title);
const excerpt = escapeXml(post.excerpt);
const content = formatContent(post.content);
const pubDate = new Date(post.date + 'T00:00:00Z').toUTCString();
const link = `https://example.com/blog/post.html?id=${post.id}`;
return ` <item>
<title>${title}</title>
<link>${link}</link>
<guid>${link}</guid>
<pubDate>${pubDate}</pubDate>
<description><![CDATA[${excerpt}<br/><br/>${content}]]></description>
</item>`;
}).join('\n');
// Generate RSS XML
const rssXml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>My Blog</title>
<link>https://example.com/blog</link>
<description>Blog posts</description>
<language>en-us</language>
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
<atom:link href="https://example.com/blog/api/feed.php" rel="self" type="application/rss+xml"/>
${rssItems}
</channel>
</rss>`;
// Write RSS feed file
fs.writeFileSync(feedPath, rssXml, 'utf8');
console.log('RSS feed generated successfully at', feedPath);
} catch (error) {
console.error('Error generating RSS feed:', error);
process.exit(1);
}
+122
View File
@@ -0,0 +1,122 @@
// Load and display a single blog post
async function loadBlogPost() {
try {
// Use relative path - this should work when post.js is loaded from /blog/post.html
const apiPath = 'api/posts.php';
const response = await fetch(apiPath);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const posts = await response.json();
// Validate that we got an array
if (!Array.isArray(posts)) {
throw new Error('Invalid response format: expected array');
}
// Get post ID from URL query parameter
const urlParams = new URLSearchParams(window.location.search);
const postId = parseInt(urlParams.get('id'));
if (isNaN(postId)) {
document.getElementById('blogPostContainer').innerHTML =
'<p class="error">Invalid post ID. Please select a post from the sidebar.</p>';
loadSidebarNavigation(posts);
return;
}
// Find the post
const post = posts.find(p => p.id === postId);
if (!post) {
document.getElementById('blogPostContainer').innerHTML =
'<p class="error">Post not found. Please select a post from the sidebar.</p>';
loadSidebarNavigation(posts);
return;
}
// Update page title
const pageTitleEl = document.getElementById('pageTitle');
if (pageTitleEl) {
pageTitleEl.textContent = `${post.title} - Launch Pad`;
}
const blogPostTitleEl = document.getElementById('blogPostTitle');
if (blogPostTitleEl) {
blogPostTitleEl.textContent = `Post-${post.id}`;
}
// Convert content newlines to paragraphs
const contentParagraphs = post.content.split('\n\n').filter(p => p.trim().length > 0);
const formattedContent = contentParagraphs.map(p => `<p>${p.trim()}</p>`).join('');
const container = document.getElementById('blogPostContainer');
container.innerHTML = `
<div class="blog-post" id="post-${post.id}">
<h2 class="blog-post-title">${post.title}</h2>
<p class="blog-post-date">${post.date}</p>
<p class="blog-post-excerpt">${post.excerpt}</p>
<div class="blog-post-content">${formattedContent}</div>
<div class="reactions-share-wrapper">
${window.blogReactions ? window.blogReactions.createReactionButtons(post.id) : ''}
<p class="blog-post-signature">-- Author --</p>
${window.blogReactions ? window.blogReactions.createShareButtons(post.id) : ''}
</div>
</div>
`;
// Load reactions for this post
if (window.blogReactions) {
window.blogReactions.loadReactions(post.id);
}
// Load sidebar navigation
loadSidebarNavigation(posts, postId);
// Align blog-sidebar top with blog-post-container top
setTimeout(() => {
const blogPostContainer = document.getElementById('blogPostContainer');
const blogSidebar = document.getElementById('blogSidebar');
if (blogPostContainer && blogSidebar) {
const containerTop = blogPostContainer.getBoundingClientRect().top;
const layoutTop = blogPostContainer.closest('.blog-layout').getBoundingClientRect().top;
const offset = containerTop - layoutTop;
blogSidebar.style.marginTop = offset + 'px';
}
}, 100);
} catch (error) {
console.error('Error loading blog post:', error);
document.getElementById('blogPostContainer').innerHTML =
'<p class="error">Error loading blog post. Please try again later.</p>';
}
}
// Load sidebar navigation with all posts
function loadSidebarNavigation(posts, currentPostId = null) {
const sidebarNav = document.getElementById('sidebarNav');
// Sort posts by date (newest first)
const sortedPosts = [...posts].sort((a, b) => new Date(b.date) - new Date(a.date));
// Clear existing navigation
sidebarNav.innerHTML = '';
sortedPosts.forEach(post => {
const navItem = document.createElement('a');
navItem.href = `post.html?id=${post.id}`;
navItem.className = 'sidebar-post-link';
navItem.textContent = `Post-${post.id}`;
// Highlight current post
if (currentPostId !== null && post.id === currentPostId) {
navItem.classList.add('active');
}
navItem.title = post.title;
sidebarNav.appendChild(navItem);
});
}
// Load post when page loads
window.addEventListener('load', loadBlogPost);
+200
View File
@@ -0,0 +1,200 @@
// Reaction and share functionality
(function() {
// Generate or retrieve visitor ID
function getVisitorId() {
let visitorId = localStorage.getItem('visitorId');
if (!visitorId) {
visitorId = 'visitor_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
localStorage.setItem('visitorId', visitorId);
}
return visitorId;
}
// Load reactions for a post
async function loadReactions(postId) {
try {
const response = await fetch(`/api/reaction.php?postId=${postId}`);
const data = await response.json();
if (data.success) {
updateReactionButtons(postId, data.counts);
}
} catch (error) {
console.error('Error loading reactions:', error);
}
}
// Send reaction
async function sendReaction(postId, reaction) {
const visitorId = getVisitorId();
try {
const response = await fetch('/api/reaction.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
postId: postId,
reaction: reaction,
visitorId: visitorId
})
});
if (!response.ok) {
console.error('API error:', response.status, response.statusText);
return;
}
const data = await response.json();
console.log('Reaction response:', data); // Debug log
if (data.success && data.counts) {
updateReactionButtons(postId, data.counts);
} else {
console.error('Invalid response:', data);
}
} catch (error) {
console.error('Error sending reaction:', error);
}
}
// Update reaction button displays
function updateReactionButtons(postId, counts) {
const container = document.querySelector(`#post-${postId} .reactions-buttons`);
if (!container) return;
['like', 'love', 'helpful'].forEach(reaction => {
const button = container.querySelector(`.reaction-btn[data-reaction="${reaction}"]`);
if (button) {
const count = counts[reaction] || 0;
const countSpan = button.querySelector('.reaction-count');
if (countSpan) {
countSpan.textContent = count;
}
}
});
}
// Track share event
async function trackShare(postId, platform) {
const visitorId = getVisitorId();
try {
await fetch('/api/track.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'share',
page: `/blog/post.html?id=${postId}`,
visitorId: visitorId,
platform: platform
})
});
} catch (error) {
console.error('Error tracking share:', error);
}
}
// Inline SVG icons - using currentColor for theme support
const iconSVGs = {
like: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 -0.5 21 21" class="reaction-icon-img"><g stroke="none" stroke-width="1" fill-rule="evenodd"><g transform="translate(-219.000000, -760.000000)" fill="currentColor"><g transform="translate(56.000000, 160.000000)"><path fill="currentColor" d="M163,610.021159 L163,618.021159 C163,619.126159 163.93975,620.000159 165.1,620.000159 L167.199999,620.000159 L167.199999,608.000159 L165.1,608.000159 C163.93975,608.000159 163,608.916159 163,610.021159 M183.925446,611.355159 L182.100546,617.890159 C181.800246,619.131159 180.639996,620.000159 179.302297,620.000159 L169.299999,620.000159 L169.299999,608.021159 L171.104948,601.826159 C171.318098,600.509159 172.754498,599.625159 174.209798,600.157159 C175.080247,600.476159 175.599997,601.339159 175.599997,602.228159 L175.599997,607.021159 C175.599997,607.573159 176.070397,608.000159 176.649997,608.000159 L181.127196,608.000159 C182.974146,608.000159 184.340196,609.642159 183.925446,611.355159"/></g></g></g></svg>',
love: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 122.88 107.41" class="reaction-icon-img"><path fill="currentColor" d="M60.83,17.19C68.84,8.84,74.45,1.62,86.79,0.21c23.17-2.66,44.48,21.06,32.78,44.41 c-3.33,6.65-10.11,14.56-17.61,22.32c-8.23,8.52-17.34,16.87-23.72,23.2l-17.4,17.26L46.46,93.56C29.16,76.9,0.95,55.93,0.02,29.95 C-0.63,11.75,13.73,0.09,30.25,0.3C45.01,0.5,51.22,7.84,60.83,17.19L60.83,17.19L60.83,17.19z"/></svg>',
helpful: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 103.19 122.88" class="reaction-icon-img"><path fill="currentColor" d="M17.16 0h82.72a3.32 3.32 0 013.31 3.31v92.32c-.15 2.58-3.48 2.64-7.08 2.48H15.94c-4.98 0-9.05 4.07-9.05 9.05s4.07 9.05 9.05 9.05h80.17v-9.63h7.08v12.24c0 2.23-1.82 4.05-4.05 4.05H16.29C7.33 122.88 0 115.55 0 106.59V17.16C0 7.72 7.72 0 17.16 0zm3.19 13.4h2.86c1.46 0 2.66.97 2.66 2.15v67.47c0 1.18-1.2 2.15-2.66 2.15h-2.86c-1.46 0-2.66-.97-2.66-2.15V15.55c.01-1.19 1.2-2.15 2.66-2.15z" fill-rule="evenodd" clip-rule="evenodd"/></svg>',
mastodon: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" class="share-icon-img"><path fill="currentColor" d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>',
bluesky: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 512 452.265" class="share-icon-img"><path fill="currentColor" d="M110.985 30.441c58.695 44.217 121.837 133.856 145.013 181.961 23.177-48.105 86.323-137.744 145.017-181.961C443.376-1.455 512-26.142 512 52.402c0 15.68-8.962 131.775-14.223 150.628-18.273 65.515-84.873 82.228-144.112 72.116 103.55 17.679 129.889 76.237 73 134.799-108.041 111.223-155.288-27.905-167.386-63.554-3.488-10.262-2.991-10.498-6.56 0-12.098 35.649-59.342 174.777-167.383 63.554-56.889-58.562-30.551-117.12 72.999-134.799-59.239 10.112-125.84-6.601-144.112-72.116C8.962 184.177 0 68.082 0 52.402 0-26.142 68.633-1.455 110.985 30.441z"/></svg>',
copy: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" class="share-icon-img"><path fill="currentColor" d="M15.24 2H11.3458C9.58159 1.99999 8.18418 1.99997 7.09054 2.1476C5.96501 2.29953 5.05402 2.61964 4.33559 3.34096C3.61717 4.06227 3.29833 4.97692 3.14701 6.10697C2.99997 7.205 2.99999 8.60802 3 10.3793V16.2169C3 17.725 3.91995 19.0174 5.22717 19.5592C5.15989 18.6498 5.15994 17.3737 5.16 16.312L5.16 11.3976L5.16 11.3024C5.15993 10.0207 5.15986 8.91644 5.27828 8.03211C5.40519 7.08438 5.69139 6.17592 6.4253 5.43906C7.15921 4.70219 8.06404 4.41485 9.00798 4.28743C9.88877 4.16854 10.9887 4.1686 12.2652 4.16867L12.36 4.16868H15.24L15.3348 4.16867C16.6113 4.1686 17.7088 4.16854 18.5896 4.28743C18.0627 2.94779 16.7616 2 15.24 2Z"/><path fill="currentColor" d="M6.6001 11.3974C6.6001 8.67119 6.6001 7.3081 7.44363 6.46118C8.28716 5.61426 9.64481 5.61426 12.3601 5.61426H15.2401C17.9554 5.61426 19.313 5.61426 20.1566 6.46118C21.0001 7.3081 21.0001 8.6712 21.0001 11.3974V16.2167C21.0001 18.9429 21.0001 20.306 20.1566 21.1529C19.313 21.9998 17.9554 21.9998 15.2401 21.9998H12.3601C9.64481 21.9998 8.28716 21.9998 7.44363 21.1529C6.6001 20.306 6.6001 18.9429 6.6001 16.2167V11.3974Z"/></svg>'
};
// Create reaction buttons HTML
function createReactionButtons(postId) {
return `
<div class="reactions-container">
<div class="reactions-label">React</div>
<div class="reactions-buttons">
<button class="reaction-btn" data-post="${postId}" data-reaction="like" title="Like">
${iconSVGs.like}<span class="reaction-count">0</span>
</button>
<button class="reaction-btn" data-post="${postId}" data-reaction="love" title="Love">
${iconSVGs.love}<span class="reaction-count">0</span>
</button>
<button class="reaction-btn" data-post="${postId}" data-reaction="helpful" title="Helpful">
${iconSVGs.helpful}<span class="reaction-count">0</span>
</button>
</div>
</div>
`;
}
// Create share buttons HTML
function createShareButtons(postId) {
return `
<div class="share-container">
<div class="share-label">Share</div>
<div class="share-buttons">
<button class="share-btn" data-platform="mastodon" data-post="${postId}" title="Share on Mastodon">${iconSVGs.mastodon}</button>
<button class="share-btn" data-platform="bluesky" data-post="${postId}" title="Share on Bluesky">${iconSVGs.bluesky}</button>
<button class="share-btn" data-platform="copy" data-post="${postId}" title="Copy link">${iconSVGs.copy}</button>
</div>
</div>
`;
}
// Handle reaction clicks
document.addEventListener('click', function(e) {
if (e.target.closest('.reaction-btn')) {
const button = e.target.closest('.reaction-btn');
const postId = button.dataset.post;
const reaction = button.dataset.reaction;
sendReaction(postId, reaction);
}
});
// Handle share clicks
document.addEventListener('click', function(e) {
if (e.target.closest('.share-btn')) {
const button = e.target.closest('.share-btn');
const platform = button.dataset.platform;
const postId = button.dataset.post;
const postTitle = document.querySelector(`#post-${postId} .blog-post-title`)?.textContent || '';
// Construct URL without port (for .onion compatibility)
const origin = window.location.protocol + '//' + window.location.hostname;
const url = origin + '/blog/post.html?id=' + postId;
if (platform === 'mastodon') {
trackShare(postId, 'mastodon');
// Open Mastodon share - user can paste the URL
const mastodonUrl = `https://defcon.social/share?text=${encodeURIComponent(postTitle + ' ' + url)}`;
window.open(mastodonUrl, '_blank', 'width=550,height=420');
} else if (platform === 'bluesky') {
trackShare(postId, 'bluesky');
// Open Bluesky share
const blueskyUrl = `https://bsky.app/intent/compose?text=${encodeURIComponent(postTitle + ' ' + url)}`;
window.open(blueskyUrl, '_blank', 'width=550,height=420');
} else if (platform === 'copy') {
trackShare(postId, 'copy');
navigator.clipboard.writeText(url).then(() => {
const icon = button.querySelector('.share-icon-img');
if (icon) {
const originalHTML = icon.outerHTML;
const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark';
const checkColor = isDarkMode ? '#59C99C' : 'currentColor';
icon.outerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" class="share-icon-img"><path fill="${checkColor}" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>`;
setTimeout(() => {
const checkIcon = button.querySelector('.share-icon-img');
if (checkIcon) {
checkIcon.outerHTML = originalHTML;
}
}, 2000);
}
});
}
}
});
// Export functions for use in blog.js
window.blogReactions = {
createReactionButtons: createReactionButtons,
createShareButtons: createShareButtons,
loadReactions: loadReactions
};
})();
+106
View File
@@ -0,0 +1,106 @@
// Generate RSS feed from blog posts
async function generateRSSFeed() {
try {
const response = await fetch('/blog/blog-posts.json');
const posts = await response.json();
// Sort posts by date (newest first)
const sortedPosts = posts.sort((a, b) => new Date(b.date) - new Date(a.date));
// Build RSS XML
const rssItems = sortedPosts.map(post => {
// Escape HTML in content for XML
const escapeXml = (str) => {
return str.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
};
const content = escapeXml(post.content);
const title = escapeXml(post.title);
const excerpt = escapeXml(post.excerpt);
const pubDate = new Date(post.date).toUTCString();
return ` <item>
<title>${title}</title>
<link>https://example.com/blogpost.html?id=${post.id}</link>
<guid>https://example.com/blogpost.html?id=${post.id}</guid>
<pubDate>${pubDate}</pubDate>
<description><![CDATA[${excerpt}<br/><br/>${content}]]></description>
</item>`;
}).join('\n');
const rssXml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>My Blog</title>
<link>https://example.com/blog</link>
<description>Blog posts</description>
<language>en-us</language>
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
<atom:link href="https://example.com/blog/feed.xml" rel="self" type="application/rss+xml"/>
${rssItems}
</channel>
</rss>`;
return rssXml;
} catch (error) {
console.error('Error generating RSS feed:', error);
return null;
}
}
// If this script is run directly (not imported), generate and serve the feed
if (typeof window === 'undefined') {
// Node.js environment - generate RSS file
const fs = require('fs');
const path = require('path');
async function writeRSSFile() {
const posts = JSON.parse(fs.readFileSync(path.join(__dirname, 'blog-posts.json'), 'utf8'));
const sortedPosts = posts.sort((a, b) => new Date(b.date) - new Date(a.date));
const escapeXml = (str) => {
return str.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
};
const rssItems = sortedPosts.map(post => {
const content = escapeXml(post.content);
const title = escapeXml(post.title);
const excerpt = escapeXml(post.excerpt);
const pubDate = new Date(post.date).toUTCString();
return ` <item>
<title>${title}</title>
<link>https://example.com/blogpost.html?id=${post.id}</link>
<guid>https://example.com/blogpost.html?id=${post.id}</guid>
<pubDate>${pubDate}</pubDate>
<description><![CDATA[${excerpt}<br/><br/>${content}]]></description>
</item>`;
}).join('\n');
const rssXml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>My Blog</title>
<link>https://example.com/blog</link>
<description>Blog posts</description>
<language>en-us</language>
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
<atom:link href="https://example.com/blog/feed.xml" rel="self" type="application/rss+xml"/>
${rssItems}
</channel>
</rss>`;
fs.writeFileSync(path.join(__dirname, 'feed.xml'), rssXml, 'utf8');
console.log('RSS feed generated successfully');
}
writeRSSFile();
}