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 app = express(); // Set options for marked to use highlight.js for syntax highlighting marked.setOptions({ highlight: function (code, language) { // Check if the language is valid 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 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; // Detect and extract the lead section const leadKeyword = ''; if (contentMarkdown.includes(leadKeyword)) { const [beforeLead, afterLead] = contentMarkdown.split(leadKeyword); // Extract the first paragraph after the lead keyword lead = afterLead.split('\n').find(line => line.trim() !== '').trim(); // Remove the lead from the main content contentMarkdown = beforeLead + afterLead.replace(lead, '').trim(); } // Convert markdown to HTML const contentHtml = marked.parse(contentMarkdown); return { contentHtml, lead }; } // Function to convert a title (with spaces and special characters) into a URL-friendly slug (with dashes) function titleToSlug(title) { return title .toLowerCase() // Convert to lowercase .replace(/[^a-z0-9\s-]/g, '') // Remove all non-alphanumeric characters except spaces and dashes .replace(/\s+/g, '-'); // Replace spaces with dashes } // Function to convert a slug (with dashes) back into a readable title (with spaces) function slugToTitle(slug) { return slug.replace(/-/g, ' '); } // Function to load all blog posts with pagination support function getAllBlogPosts(page = 1, postsPerPage = 5) { const blogFiles = fs.readdirSync(path.join(__dirname, 'markdown')).filter(file => file.endsWith('.md')); // Paginate the results 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, ' '); // Keep original casing for title const slug = titleToSlug(title); // Convert title to slug (lowercase) // Get the last modified date of the markdown file const stats = fs.statSync(path.join(__dirname, 'markdown', file)); const dateCreated = new Date(stats.birthtime); // Use birthtime for last modification time // Format the date const formattedDate = dateCreated.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); return { title, // Original casing title slug, date: formattedDate // Include the formatted date }; }); return { blogPosts, totalPages }; } // Home Route (Blog Home with Pagination) app.get('/', (req, res) => { const page = parseInt(req.query.page) || 1; if (page < 1) { return res.redirect(req.hostname); } const postsPerPage = 5; // Set how many posts to display per page const { blogPosts, totalPages } = getAllBlogPosts(page, postsPerPage); res.render('index', { title: 'Raven Scott Blog', blogPosts, currentPage: page, totalPages }); }); // About Route app.get('/about', (req, res) => { res.render('about', { title: 'About Raven Scott' }); }); // Display the Request a Quote form app.get('/contact', (req, res) => { res.render('contact', { title: 'Contact Raven Scott', msg: undefined }); }); // Handle contact form submission app.post('/contact', (req, res) => { const { name, email, subject, message } = req.body; // Validate form inputs (basic example) if (!name || !email || !subject || !message) { return res.render('contact', { title: 'Contact Raven Scott', msg: 'All fields are required.' }); } // Create email content const output = `

You have a new contact request from ${name}.

Contact Details

Message

${message}

`; // Set up Nodemailer transporter let transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, port: process.env.SMTP_PORT, secure: false, // true for 465, false for other ports auth: { user: process.env.EMAIL_USER, // Email user from environment variables pass: process.env.EMAIL_PASS, // Email password from environment variables }, tls: { rejectUnauthorized: false, }, }); // Set up email options let mailOptions = { from: `"${name}" `, to: process.env.RECEIVER_EMAIL, // Your email address to receive contact form submissions subject: subject, html: output, }; // Send email transporter.sendMail(mailOptions, (error, info) => { if (error) { console.error(error); return res.render('contact', { title: 'Contact Raven Scott', msg: 'An error occurred. Please try again.' }); } else { console.log('Email sent: ' + info.response); return res.render('contact', { title: 'Contact Raven Scott', msg: 'Your message has been sent successfully!' }); } }); }); // 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', ''); // Original title with casing const blogPosts = getAllBlogPosts(); const { contentHtml, lead } = loadMarkdownWithLead(markdownFile); res.render('blog-post', { title: originalTitle, // Use the original title with casing content: contentHtml, lead: lead, blogPosts }); } else { res.redirect('/'); // Redirect to the home page if the blog post is not found } }); // Sitemap Route // Sitemap Route app.get('/sitemap.xml', (req, res) => { const hostname = req.headers.host || 'http://localhost'; // Ensure this is your site URL in production 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; // Sort in descending order (latest first) }); // Static URLs (e.g., homepage, about, contact) const staticUrls = [ { url: '/', changefreq: 'weekly', priority: 1.0 }, { url: '/about', changefreq: 'monthly', priority: 0.8 }, { url: '/contact', changefreq: 'monthly', priority: 0.8 } ]; // Dynamic URLs (e.g., blog posts) const blogUrls = blogFiles.map(file => { const title = file.replace('.md', ''); const slug = titleToSlug(title); // Get the last modified date of the markdown file const stats = fs.statSync(path.join(__dirname, 'markdown', file)); const lastModifiedDate = format(new Date(stats.birthtime), 'yyyy-MM-dd'); return { url: `/blog/${slug}`, lastmod: lastModifiedDate, changefreq: 'monthly', priority: 0.9 }; }); // Combine static and dynamic URLs const urls = [...staticUrls, ...blogUrls]; // Generate the XML for the sitemap let sitemap = `\n\n`; urls.forEach(({ url, lastmod, changefreq, priority }) => { sitemap += ` \n`; sitemap += ` https://${hostname}${url}\n`; if (lastmod) { sitemap += ` ${lastmod}\n`; } sitemap += ` ${changefreq}\n`; sitemap += ` ${priority}\n`; sitemap += ` \n`; }); sitemap += ``; // Set the content type to XML and send the response res.header('Content-Type', 'application/xml'); res.send(sitemap); }); // RSS Feed Route app.get('/rss', (req, res) => { const hostname = req.headers.host || 'http://localhost'; // Adjust for production if needed 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; // Sort in descending order (latest first) }); // Build RSS feed let rssFeed = `\n\n\n`; rssFeed += `Raven Scott Blog\n`; rssFeed += `https://${hostname}\n`; rssFeed += `This is the RSS feed for Raven Scott's blog.\n`; // Generate RSS items for each blog post blogFiles.forEach(file => { const title = file.replace('.md', ''); const slug = titleToSlug(title); // Get the last modified date of the markdown file const stats = fs.statSync(path.join(__dirname, 'markdown', file)); const lastModifiedDate = new Date(stats.birthtime).toUTCString(); // Use UTC date for RSS // Load and parse markdown content to extract a lead or description const { lead } = loadMarkdownWithLead(file); // RSS item for each post rssFeed += `\n`; rssFeed += `${title}\n`; rssFeed += `https://${hostname}/blog/${slug}\n`; rssFeed += `${lead || 'Read the full post on the blog.'}\n`; rssFeed += `${lastModifiedDate}\n`; rssFeed += `https://${hostname}/blog/${slug}\n`; rssFeed += `\n`; }); rssFeed += `\n`; // Set content type to XML and send the RSS feed res.header('Content-Type', 'application/rss+xml'); res.send(rssFeed); }); // Global 404 handler for any other unmatched routes app.use((req, res) => { res.redirect('/'); // Redirect to the home page for any 404 error }); // ================================ // Server Listening // ================================ const PORT = process.env.PORT || 8899; app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); });