ravenscott-blog/app.js

304 lines
10 KiB
JavaScript
Raw Normal View History

2024-09-16 07:53:26 -04:00
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');
2024-09-16 14:22:37 -04:00
const { format } = require('date-fns'); // To format dates in a proper XML format
2024-09-19 08:30:19 -04:00
const axios = require('axios'); // Add axios for reCAPTCHA verification
2024-09-16 07:53:26 -04:00
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')));
2024-09-26 18:13:47 -04:00
// 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 titleRegex = /<!--\s*title:\s*(.*?)\s*-->/g;
const urlRegex = /<!--\s*url:\s*(.*?)\s*-->/g;
const openNewPageRegex = /<!--\s*openNewPage\s*-->/g;
2024-09-26 18:13:47 -04:00
let titleMatch;
let urlMatch;
let openNewPageMatch;
2024-09-26 18:13:47 -04:00
// Use a loop to find and process each menu item
2024-09-26 18:13:47 -04:00
while ((titleMatch = titleRegex.exec(content)) && (urlMatch = urlRegex.exec(content))) {
openNewPageMatch = openNewPageRegex.exec(content);
2024-09-26 18:13:47 -04:00
menuItems.push({
title: titleMatch[1],
url: urlMatch[1],
openNewPage: openNewPageMatch !== null // If openNewPage exists, set it to true
2024-09-26 18:13:47 -04:00
});
}
return menuItems;
}
// Load the menu once and make it available to all routes
const menuItems = loadMenuItems();
2024-09-16 07:53:26 -04:00
// Function to load and parse markdown files and extract lead
function loadMarkdownWithLead(file) {
const markdownContent = fs.readFileSync(path.join(__dirname, 'markdown', file), 'utf-8');
2024-09-16 15:53:31 -04:00
2024-09-16 07:53:26 -04:00
let lead = '';
let contentMarkdown = markdownContent;
const leadKeyword = '<!-- lead -->';
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 };
}
2024-09-26 01:27:35 -04:00
// Function to convert a title into a URL-friendly slug
2024-09-16 07:53:26 -04:00
function titleToSlug(title) {
return title
2024-09-26 01:27:35 -04:00
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-');
2024-09-16 07:53:26 -04:00
}
2024-09-26 01:27:35 -04:00
// 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));
}
2024-09-16 13:42:27 -04:00
if (blogFiles.length === 0) {
return { blogPosts: [], totalPages: 0 }; // Return empty results if no files
}
2024-09-25 23:05:49 -04:00
blogFiles.sort((a, b) => {
const statA = fs.statSync(path.join(__dirname, 'markdown', a)).birthtime;
const statB = fs.statSync(path.join(__dirname, 'markdown', b)).birthtime;
2024-09-26 01:27:35 -04:00
return statB - statA;
2024-09-25 23:05:49 -04:00
});
2024-09-16 13:42:27 -04:00
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);
2024-09-16 15:53:31 -04:00
2024-09-16 13:42:27 -04:00
const blogPosts = paginatedFiles.map(file => {
2024-09-26 01:27:35 -04:00
const title = file.replace('.md', '').replace(/-/g, ' ');
const slug = titleToSlug(title);
2024-09-16 13:42:27 -04:00
const stats = fs.statSync(path.join(__dirname, 'markdown', file));
2024-09-26 01:27:35 -04:00
const dateCreated = new Date(stats.birthtime);
2024-09-16 13:42:27 -04:00
2024-09-26 01:27:35 -04:00
return { title, slug, dateCreated };
2024-09-16 07:53:26 -04:00
});
2024-09-16 13:42:27 -04:00
return { blogPosts, totalPages };
2024-09-16 07:53:26 -04:00
}
2024-09-26 01:27:35 -04:00
// Home Route (Blog Home with Pagination and Search)
2024-09-16 07:53:26 -04:00
app.get('/', (req, res) => {
2024-09-16 13:42:27 -04:00
const page = parseInt(req.query.page) || 1;
2024-09-26 01:27:35 -04:00
const searchQuery = req.query.search || '';
2024-09-16 15:53:31 -04:00
if (page < 1) {
return res.redirect(req.hostname);
}
2024-09-26 01:27:35 -04:00
const postsPerPage = 5;
const { blogPosts, totalPages } = getAllBlogPosts(page, postsPerPage, searchQuery);
2024-09-16 15:53:31 -04:00
const noResults = blogPosts.length === 0; // Check if there are no results
2024-09-16 13:42:27 -04:00
res.render('index', {
title: `${process.env.OWNER_NAME}'s Blog`,
2024-09-16 13:42:27 -04:00
blogPosts,
currentPage: page,
2024-09-26 01:27:35 -04:00
totalPages,
searchQuery, // Pass search query to the view
2024-09-26 18:13:47 -04:00
noResults, // Pass this flag to indicate no results found
menuItems // Pass the menu items to the view
2024-09-16 13:42:27 -04:00
});
2024-09-16 07:53:26 -04:00
});
2024-09-26 02:41:22 -04:00
// About Route (Load markdown and render using EJS)
2024-09-16 07:53:26 -04:00
app.get('/about', (req, res) => {
2024-09-26 02:41:22 -04:00
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}`,
2024-09-26 18:13:47 -04:00
content: aboutContentHtml,
menuItems // Pass the menu items to the view
2024-09-26 02:41:22 -04:00
});
});
2024-09-16 07:53:26 -04:00
});
2024-09-26 18:13:47 -04:00
// Contact Route (Render the contact form)
2024-09-16 07:53:26 -04:00
app.get('/contact', (req, res) => {
2024-09-26 18:13:47 -04:00
res.render('contact', {
title: `Contact ${process.env.OWNER_NAME}`,
msg: undefined,
menuItems // Pass the menu items to the view
});
2024-09-16 08:07:36 -04:00
});
2024-09-26 00:03:45 -04:00
// Blog Post Route
2024-09-16 07:53:26 -04:00
app.get('/blog/:slug', (req, res) => {
const slug = req.params.slug;
const markdownFile = fs.readdirSync(path.join(__dirname, 'markdown'))
2024-09-16 15:53:31 -04:00
.find(file => titleToSlug(file.replace('.md', '')) === slug);
2024-09-16 07:53:26 -04:00
if (markdownFile) {
2024-09-26 01:27:35 -04:00
const originalTitle = markdownFile.replace('.md', '');
2024-09-16 07:53:26 -04:00
const blogPosts = getAllBlogPosts();
const { contentHtml, lead } = loadMarkdownWithLead(markdownFile);
2024-09-26 04:55:38 -04:00
// 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.`;
2024-09-16 07:53:26 -04:00
res.render('blog-post', {
2024-09-26 01:27:35 -04:00
title: originalTitle,
2024-09-16 07:53:26 -04:00
content: contentHtml,
2024-09-26 04:55:38 -04:00
lead,
description, // Pass the description to the view
2024-09-26 18:13:47 -04:00
blogPosts,
menuItems // Pass the menu items to the view
2024-09-16 07:53:26 -04:00
});
} else {
2024-09-26 01:27:35 -04:00
res.redirect('/');
2024-09-16 07:53:26 -04:00
}
2024-09-26 00:03:45 -04:00
});
2024-09-16 14:22:37 -04:00
// Sitemap Route
app.get('/sitemap.xml', (req, res) => {
2024-09-26 01:27:35 -04:00
const hostname = req.headers.host || 'http://localhost';
2024-09-18 18:55:52 -04:00
const blogFiles = fs.readdirSync(path.join(__dirname, 'markdown'))
.filter(file => file.endsWith('.md'))
.sort((a, b) => {
2024-09-19 01:45:44 -04:00
const statA = fs.statSync(path.join(__dirname, 'markdown', a)).birthtime;
const statB = fs.statSync(path.join(__dirname, 'markdown', b)).birthtime;
2024-09-26 01:27:35 -04:00
return statB - statA;
2024-09-18 18:55:52 -04:00
});
2024-09-16 14:22:37 -04:00
const staticUrls = [
2024-09-26 01:05:37 -04:00
{ 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 }
2024-09-16 14:22:37 -04:00
];
const blogUrls = blogFiles.map(file => {
const title = file.replace('.md', '');
const slug = titleToSlug(title);
const stats = fs.statSync(path.join(__dirname, 'markdown', file));
2024-09-19 01:45:44 -04:00
const lastModifiedDate = format(new Date(stats.birthtime), 'yyyy-MM-dd');
2024-09-16 14:22:37 -04:00
return {
2024-09-26 01:05:37 -04:00
url: `${process.env.BLOG_URL}${slug}`,
2024-09-16 14:22:37 -04:00
lastmod: lastModifiedDate,
changefreq: 'monthly',
priority: 0.9
};
});
const urls = [...staticUrls, ...blogUrls];
let sitemap = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n`;
urls.forEach(({ url, lastmod, changefreq, priority }) => {
sitemap += ` <url>\n`;
2024-09-26 00:20:45 -04:00
sitemap += ` <loc>${url}</loc>\n`;
2024-09-16 14:22:37 -04:00
if (lastmod) {
sitemap += ` <lastmod>${lastmod}</lastmod>\n`;
}
sitemap += ` <changefreq>${changefreq}</changefreq>\n`;
sitemap += ` <priority>${priority}</priority>\n`;
sitemap += ` </url>\n`;
});
sitemap += `</urlset>`;
res.header('Content-Type', 'application/xml');
res.send(sitemap);
});
2024-09-16 14:28:18 -04:00
// RSS Feed Route
app.get('/rss', (req, res) => {
2024-09-26 01:27:35 -04:00
const hostname = req.headers.host || 'http://localhost';
2024-09-18 18:55:52 -04:00
const blogFiles = fs.readdirSync(path.join(__dirname, 'markdown'))
.filter(file => file.endsWith('.md'))
.sort((a, b) => {
2024-09-19 01:45:44 -04:00
const statA = fs.statSync(path.join(__dirname, 'markdown', a)).birthtime;
const statB = fs.statSync(path.join(__dirname, 'markdown', b)).birthtime;
2024-09-26 01:27:35 -04:00
return statB - statA;
2024-09-18 18:55:52 -04:00
});
2024-09-16 14:28:18 -04:00
let rssFeed = `<?xml version="1.0" encoding="UTF-8" ?>\n<rss version="2.0">\n<channel>\n`;
2024-09-26 01:51:44 -04:00
rssFeed += `<title>${process.env.OWNER_NAME} Blog</title>\n`;
2024-09-16 14:28:18 -04:00
rssFeed += `<link>https://${hostname}</link>\n`;
2024-09-26 01:51:44 -04:00
rssFeed += `<description>This is the RSS feed for ${process.env.OWNER_NAME}'s blog.</description>\n`;
2024-09-16 14:28:18 -04:00
blogFiles.forEach(file => {
const title = file.replace('.md', '');
const slug = titleToSlug(title);
const stats = fs.statSync(path.join(__dirname, 'markdown', file));
2024-09-26 01:27:35 -04:00
const lastModifiedDate = new Date(stats.birthtime).toUTCString();
2024-09-16 14:28:18 -04:00
const { lead } = loadMarkdownWithLead(file);
rssFeed += `<item>\n`;
rssFeed += `<title>${title}</title>\n`;
2024-09-26 01:05:37 -04:00
rssFeed += `<link>${process.env.BLOG_URL}${slug}</link>\n`;
2024-09-16 14:28:18 -04:00
rssFeed += `<description>${lead || 'Read the full post on the blog.'}</description>\n`;
rssFeed += `<pubDate>${lastModifiedDate}</pubDate>\n`;
2024-09-26 01:05:37 -04:00
rssFeed += `<guid>${process.env.BLOG_URL}${slug}</guid>\n`;
2024-09-16 14:28:18 -04:00
rssFeed += `</item>\n`;
});
rssFeed += `</channel>\n</rss>`;
res.header('Content-Type', 'application/rss+xml');
res.send(rssFeed);
});
2024-09-26 00:18:59 -04:00
// Global 404 handler for unmatched routes
2024-09-16 08:21:29 -04:00
app.use((req, res) => {
2024-09-26 18:13:47 -04:00
res.redirect('/');
2024-09-16 08:21:29 -04:00
});
2024-09-16 07:53:26 -04:00
// Server Listening
2024-09-16 07:55:16 -04:00
const PORT = process.env.PORT || 8899;
2024-09-16 07:53:26 -04:00
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});