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 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 convert a slug back into a readable title function slugToTitle(slug) { return slug.replace(/-/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 }); }); // About Route app.get('/about', (req, res) => { res.render('about', { title: `About ${process.env.OWNER_NAME}` }); }); // Display the Request a Quote form app.get('/contact', (req, res) => { res.render('contact', { title: `Contact ${process.env.OWNER_NAME}`, msg: undefined }); }); // Handle contact form submission app.post('/contact', async (req, res) => { const { name, email, subject, message, 'g-recaptcha-response': captchaToken } = req.body; if (!name || !email || !subject || !message) { return res.render('contact', { title: `Contact ${process.env.OWNER_NAME}`, msg: 'All fields are required.' }); } const captchaSecret = process.env.CAPTCHA_SECRET_KEY; const captchaVerifyUrl = `https://www.google.com/recaptcha/api/siteverify?secret=${captchaSecret}&response=${captchaToken}`; try { const captchaResponse = await axios.post(captchaVerifyUrl); if (!captchaResponse.data.success) { return res.render('contact', { title: `Contact ${process.env.OWNER_NAME}`, msg: 'Captcha verification failed. Please try again.' }); } const output = `
You have a new contact request from ${name}.
${message}
`; let transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, port: process.env.SMTP_PORT, secure: false, auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASS, }, tls: { rejectUnauthorized: false }, }); let mailOptions = { from: `"${name}" <${process.env.RECEIVER_EMAIL}>`, to: process.env.RECEIVER_EMAIL, subject: subject, html: output, }; transporter.sendMail(mailOptions, (error, info) => { if (error) { return res.render('contact', { title: `Contact ${process.env.OWNER_NAME}`, msg: 'An error occurred. Please try again.' }); } return res.render('contact', { title: `Contact ${process.env.OWNER_NAME}`, msg: 'Your message has been sent successfully!' }); }); } catch (error) { return res.render('contact', { title: `Contact ${process.env.OWNER_NAME}`, msg: 'An error occurred while verifying CAPTCHA. Please try again.' }); } }); // 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); res.render('blog-post', { title: originalTitle, content: contentHtml, lead: lead, blogPosts }); } 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