From 8e02173583853e29ba13859a0b50c8042c80352c Mon Sep 17 00:00:00 2001 From: Raven Scott Date: Thu, 26 Sep 2024 01:27:35 -0400 Subject: [PATCH] Add search feature --- app.js | 124 +++++++++++++++--------------------------------- views/index.ejs | 17 +++++-- 2 files changed, 51 insertions(+), 90 deletions(-) diff --git a/app.js b/app.js index 011915a..df76b03 100644 --- a/app.js +++ b/app.js @@ -13,7 +13,6 @@ 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; } @@ -36,49 +35,45 @@ function loadMarkdownWithLead(file) { 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 to convert a title into a URL-friendly slug 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 + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, '') + .replace(/\s+/g, '-'); } -// Function to convert a slug (with dashes) back into a readable title (with spaces) +// 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 support -function getAllBlogPosts(page = 1, postsPerPage = 5) { - const blogFiles = fs.readdirSync(path.join(__dirname, 'markdown')).filter(file => file.endsWith('.md')); +// 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)); + } - // Sort by birthtime (latest first) like in your RSS feed 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; // Descending order, latest first + return statB - statA; }); - // Paginate the results const totalPosts = blogFiles.length; const totalPages = Math.ceil(totalPosts / postsPerPage); const start = (page - 1) * postsPerPage; @@ -87,41 +82,35 @@ function getAllBlogPosts(page = 1, postsPerPage = 5) { 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 creation time of the markdown 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); // Use birthtime for sorting and displaying + const dateCreated = new Date(stats.birthtime); - return { - title, - slug, - dateCreated, // Keep as Date object for sorting - }; + return { title, slug, dateCreated }; }); - // Return the paginated and sorted blog posts and the total pages return { blogPosts, totalPages }; } -// Home Route (Blog Home with Pagination) +// 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; // Set how many posts to display per page - - const { blogPosts, totalPages } = getAllBlogPosts(page, postsPerPage); + const postsPerPage = 5; + const { blogPosts, totalPages } = getAllBlogPosts(page, postsPerPage, searchQuery); res.render('index', { title: 'Raven Scott Blog', blogPosts, currentPage: page, - totalPages + totalPages, + searchQuery // Pass search query to the view }); }); @@ -139,23 +128,19 @@ app.get('/contact', (req, res) => { app.post('/contact', async (req, res) => { const { name, email, subject, message, 'g-recaptcha-response': captchaToken } = 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.' }); } - // Verify the reCAPTCHA token - const captchaSecret = process.env.CAPTCHA_SECRET_KEY; // Your reCAPTCHA secret key + 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 Raven Scott', msg: 'Captcha verification failed. Please try again.' }); } - // CAPTCHA passed, proceed with sending email const output = `

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

Contact Details

@@ -168,40 +153,31 @@ app.post('/contact', async (req, res) => {

${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 + secure: false, auth: { - user: process.env.EMAIL_USER, // Email user from environment variables - pass: process.env.EMAIL_PASS, // Email password from environment variables - }, - tls: { - rejectUnauthorized: false, + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, }, + tls: { rejectUnauthorized: false }, }); - // Set up email options let mailOptions = { from: `"${name}" <${process.env.RECEIVER_EMAIL}>`, - to: process.env.RECEIVER_EMAIL, // Your email address to receive contact form submissions + to: process.env.RECEIVER_EMAIL, 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!' }); } + return res.render('contact', { title: 'Contact Raven Scott', msg: 'Your message has been sent successfully!' }); }); } catch (error) { - console.error('Error verifying CAPTCHA:', error); return res.render('contact', { title: 'Contact Raven Scott', msg: 'An error occurred while verifying CAPTCHA. Please try again.' }); } }); @@ -213,46 +189,41 @@ app.get('/blog/:slug', (req, res) => { .find(file => titleToSlug(file.replace('.md', '')) === slug); if (markdownFile) { - const originalTitle = markdownFile.replace('.md', ''); // Original title with casing + const originalTitle = markdownFile.replace('.md', ''); const blogPosts = getAllBlogPosts(); const { contentHtml, lead } = loadMarkdownWithLead(markdownFile); res.render('blog-post', { - title: originalTitle, // Use the original title with casing + title: originalTitle, content: contentHtml, lead: lead, blogPosts }); } else { - res.redirect('/'); // Redirect to the home page if the blog post is not found + res.redirect('/'); } }); -// 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 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; // Sort in descending order (latest first) + return statB - statA; }); - // Static URLs (e.g., homepage, about, contact) 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 } ]; - // 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'); @@ -264,10 +235,8 @@ app.get('/sitemap.xml', (req, res) => { }; }); - // 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`; @@ -281,42 +250,33 @@ app.get('/sitemap.xml', (req, res) => { }); 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 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; // Sort in descending order (latest first) + return statB - statA; }); - // 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 lastModifiedDate = new Date(stats.birthtime).toUTCString(); const { lead } = loadMarkdownWithLead(file); - // RSS item for each post rssFeed += `\n`; rssFeed += `${title}\n`; rssFeed += `${process.env.BLOG_URL}${slug}\n`; @@ -327,8 +287,6 @@ app.get('/rss', (req, res) => { }); rssFeed += `\n`; - - // Set content type to XML and send the RSS feed res.header('Content-Type', 'application/rss+xml'); res.send(rssFeed); }); @@ -336,17 +294,13 @@ app.get('/rss', (req, res) => { // Global 404 handler for unmatched routes app.use((req, res) => { if (req.hostname === 'blog.raven-scott.fyi') { - // Redirect to the main domain res.redirect(process.env.HOST_URL); } else { - // Redirect to home page of the current domain res.redirect('/'); } }); -// ================================ // Server Listening -// ================================ const PORT = process.env.PORT || 8899; app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); diff --git a/views/index.ejs b/views/index.ejs index 322a26f..34bca54 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -35,12 +35,21 @@

Hello, my name is Raven Scott

Where Technology Meets Creativity: Insights from a Linux Enthusiast

+
+
+ + +
+
- + +
-

Recent Posts

+ + +

<%= searchQuery ? `Search results for "${searchQuery}"` : 'Recent Posts' %>

    <% blogPosts.forEach(post => { %>
  • @@ -58,9 +67,7 @@
  • <% }) %>
-
-
- +