require('dotenv').config(); // Load environment variables const express = require('express'); const path = require('path'); const fs = require('fs'); const { marked } = require('marked'); const nodemailer = require('nodemailer'); const hljs = require('highlight.js'); const { format } = require('date-fns'); // To format dates in a proper XML format const axios = require('axios'); // Add axios for reCAPTCHA verification const app = express(); // Set options for marked to use highlight.js for syntax highlighting marked.setOptions({ highlight: function (code, language) { const validLanguage = hljs.getLanguage(language) ? language : 'plaintext'; return hljs.highlight(validLanguage, code).value; } }); // Set EJS as templating engine app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); // Middleware to parse URL-encoded bodies (form submissions) app.use(express.urlencoded({ extended: false })); // Serve static files (CSS, Images) app.use(express.static(path.join(__dirname, 'public'))); // Function to load menu items from the markdown file function loadMenuItems() { const menuFile = path.join(__dirname, 'menu.md'); const content = fs.readFileSync(menuFile, 'utf-8'); const menuItems = []; const itemRegex = /\s*(\s*)?/g; let match; // Loop to find all menu items while ((match = itemRegex.exec(content)) !== null) { const title = match[1]; const url = match[3]; const openNewPage = !!match[2]; // Check if openNewPage is present in the match menuItems.push({ title, url, openNewPage }); } return menuItems; } // Load the menu once and make it available to all routes const menuItems = loadMenuItems(); // Function to load and parse markdown files and extract lead function loadMarkdownWithLead(file) { const markdownContent = fs.readFileSync(path.join(__dirname, 'markdown', file), 'utf-8'); let lead = ''; let contentMarkdown = markdownContent; const leadKeyword = ''; if (contentMarkdown.includes(leadKeyword)) { const [beforeLead, afterLead] = contentMarkdown.split(leadKeyword); lead = afterLead.split('\n').find(line => line.trim() !== '').trim(); contentMarkdown = beforeLead + afterLead.replace(lead, '').trim(); } const contentHtml = marked.parse(contentMarkdown); return { contentHtml, lead }; } // Function to convert a title into a URL-friendly slug function titleToSlug(title) { return title .toLowerCase() .replace(/[^a-z0-9\s-]/g, '') .replace(/\s+/g, '-'); } // Function to load all blog posts with pagination and search support function getAllBlogPosts(page = 1, postsPerPage = 5, searchQuery = '') { let blogFiles = fs.readdirSync(path.join(__dirname, 'markdown')).filter(file => file.endsWith('.md')); if (searchQuery) { const lowerCaseQuery = searchQuery.toLowerCase(); blogFiles = blogFiles.filter(file => file.toLowerCase().includes(lowerCaseQuery)); } if (blogFiles.length === 0) { return { blogPosts: [], totalPages: 0 }; // Return empty results if no files } blogFiles.sort((a, b) => { const statA = fs.statSync(path.join(__dirname, 'markdown', a)).birthtime; const statB = fs.statSync(path.join(__dirname, 'markdown', b)).birthtime; return statB - statA; }); const totalPosts = blogFiles.length; const totalPages = Math.ceil(totalPosts / postsPerPage); const start = (page - 1) * postsPerPage; const end = start + postsPerPage; const paginatedFiles = blogFiles.slice(start, end); const blogPosts = paginatedFiles.map(file => { const title = file.replace('.md', '').replace(/-/g, ' '); const slug = titleToSlug(title); const stats = fs.statSync(path.join(__dirname, 'markdown', file)); const dateCreated = new Date(stats.birthtime); return { title, slug, dateCreated }; }); return { blogPosts, totalPages }; } // Home Route (Blog Home with Pagination and Search) app.get('/', (req, res) => { const page = parseInt(req.query.page) || 1; const searchQuery = req.query.search || ''; if (page < 1) { return res.redirect(req.hostname); } const postsPerPage = 5; const { blogPosts, totalPages } = getAllBlogPosts(page, postsPerPage, searchQuery); const noResults = blogPosts.length === 0; // Check if there are no results res.render('index', { title: `${process.env.OWNER_NAME}'s Blog`, blogPosts, currentPage: page, totalPages, searchQuery, // Pass search query to the view noResults, // Pass this flag to indicate no results found menuItems // Pass the menu items to the view }); }); // About Route (Load markdown and render using EJS) app.get('/about', (req, res) => { const aboutMarkdownFile = path.join(__dirname, 'me', 'about.md'); // Read the markdown file and convert to HTML fs.readFile(aboutMarkdownFile, 'utf-8', (err, data) => { if (err) { return res.status(500).send('Error loading About page'); } const aboutContentHtml = marked(data); // Convert markdown to HTML res.render('about', { title: `About ${process.env.OWNER_NAME}`, content: aboutContentHtml, menuItems // Pass the menu items to the view }); }); }); // Contact Route (Render the contact form) app.get('/contact', (req, res) => { res.render('contact', { title: `Contact ${process.env.OWNER_NAME}`, msg: undefined, menuItems // Pass the menu items to the view }); }); // Contact Route (Render the contact form) app.get('/chat', (req, res) => { res.render('chat', { title: `RayAI - Raven's Chatbot`, msg: undefined, menuItems // Pass the menu items to the view }); }); // Blog Post Route app.get('/blog/:slug', (req, res) => { const slug = req.params.slug; const markdownFile = fs.readdirSync(path.join(__dirname, 'markdown')) .find(file => titleToSlug(file.replace('.md', '')) === slug); if (markdownFile) { const originalTitle = markdownFile.replace('.md', ''); const blogPosts = getAllBlogPosts(); const { contentHtml, lead } = loadMarkdownWithLead(markdownFile); // Fallback to a generic description if lead is not available const description = lead || `${originalTitle} - Read the full post on ${process.env.OWNER_NAME}'s blog.`; res.render('blog-post', { title: originalTitle, content: contentHtml, lead, description, // Pass the description to the view blogPosts, menuItems // Pass the menu items to the view }); } else { res.redirect('/'); } }); // Sitemap Route app.get('/sitemap.xml', (req, res) => { const hostname = req.headers.host || 'http://localhost'; const blogFiles = fs.readdirSync(path.join(__dirname, 'markdown')) .filter(file => file.endsWith('.md')) .sort((a, b) => { const statA = fs.statSync(path.join(__dirname, 'markdown', a)).birthtime; const statB = fs.statSync(path.join(__dirname, 'markdown', b)).birthtime; return statB - statA; }); const staticUrls = [ { url: `${process.env.HOST_URL}`, changefreq: 'weekly', priority: 1.0 }, { url: `${process.env.HOST_URL}/about`, changefreq: 'monthly', priority: 0.8 }, { url: `${process.env.HOST_URL}/contact`, changefreq: 'monthly', priority: 0.8 } ]; const blogUrls = blogFiles.map(file => { const title = file.replace('.md', ''); const slug = titleToSlug(title); const stats = fs.statSync(path.join(__dirname, 'markdown', file)); const lastModifiedDate = format(new Date(stats.birthtime), 'yyyy-MM-dd'); return { url: `${process.env.BLOG_URL}${slug}`, lastmod: lastModifiedDate, changefreq: 'monthly', priority: 0.9 }; }); const urls = [...staticUrls, ...blogUrls]; let sitemap = `\n\n`; urls.forEach(({ url, lastmod, changefreq, priority }) => { sitemap += ` \n`; sitemap += ` ${url}\n`; if (lastmod) { sitemap += ` ${lastmod}\n`; } sitemap += ` ${changefreq}\n`; sitemap += ` ${priority}\n`; sitemap += ` \n`; }); sitemap += ``; res.header('Content-Type', 'application/xml'); res.send(sitemap); }); // RSS Feed Route app.get('/rss', (req, res) => { const hostname = req.headers.host || 'http://localhost'; const blogFiles = fs.readdirSync(path.join(__dirname, 'markdown')) .filter(file => file.endsWith('.md')) .sort((a, b) => { const statA = fs.statSync(path.join(__dirname, 'markdown', a)).birthtime; const statB = fs.statSync(path.join(__dirname, 'markdown', b)).birthtime; return statB - statA; }); let rssFeed = `\n\n\n`; rssFeed += `${process.env.OWNER_NAME} Blog\n`; rssFeed += `https://${hostname}\n`; rssFeed += `This is the RSS feed for ${process.env.OWNER_NAME}'s blog.\n`; blogFiles.forEach(file => { const title = file.replace('.md', ''); const slug = titleToSlug(title); const stats = fs.statSync(path.join(__dirname, 'markdown', file)); const lastModifiedDate = new Date(stats.birthtime).toUTCString(); const { lead } = loadMarkdownWithLead(file); rssFeed += `\n`; rssFeed += `${title}\n`; rssFeed += `${process.env.BLOG_URL}${slug}\n`; rssFeed += `${lead || 'Read the full post on the blog.'}\n`; rssFeed += `${lastModifiedDate}\n`; rssFeed += `${process.env.BLOG_URL}${slug}\n`; rssFeed += `\n`; }); rssFeed += `\n`; res.header('Content-Type', 'application/rss+xml'); res.send(rssFeed); }); // Create a URL object from the environment variable const blog_URL = new URL(process.env.BLOG_URL); // Extract just the hostname (e.g., blog.raven-scott.fyi) const hostname = blog_URL.hostname; // Global 404 handler for unmatched routes app.use((req, res) => { if (req.hostname === hostname) { res.redirect(process.env.HOST_URL); } else { res.redirect('/'); } }); // Server Listening const PORT = process.env.PORT || 8899; app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); });