forked from snxraven/ravenscott-blog
Add search feature
This commit is contained in:
parent
5dc2d2e6bc
commit
8e02173583
124
app.js
124
app.js
@ -13,7 +13,6 @@ const app = express();
|
|||||||
// Set options for marked to use highlight.js for syntax highlighting
|
// Set options for marked to use highlight.js for syntax highlighting
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
highlight: function (code, language) {
|
highlight: function (code, language) {
|
||||||
// Check if the language is valid
|
|
||||||
const validLanguage = hljs.getLanguage(language) ? language : 'plaintext';
|
const validLanguage = hljs.getLanguage(language) ? language : 'plaintext';
|
||||||
return hljs.highlight(validLanguage, code).value;
|
return hljs.highlight(validLanguage, code).value;
|
||||||
}
|
}
|
||||||
@ -36,49 +35,45 @@ function loadMarkdownWithLead(file) {
|
|||||||
let lead = '';
|
let lead = '';
|
||||||
let contentMarkdown = markdownContent;
|
let contentMarkdown = markdownContent;
|
||||||
|
|
||||||
// Detect and extract the lead section
|
|
||||||
const leadKeyword = '<!-- lead -->';
|
const leadKeyword = '<!-- lead -->';
|
||||||
if (contentMarkdown.includes(leadKeyword)) {
|
if (contentMarkdown.includes(leadKeyword)) {
|
||||||
const [beforeLead, afterLead] = contentMarkdown.split(leadKeyword);
|
const [beforeLead, afterLead] = contentMarkdown.split(leadKeyword);
|
||||||
|
|
||||||
// Extract the first paragraph after the lead keyword
|
|
||||||
lead = afterLead.split('\n').find(line => line.trim() !== '').trim();
|
lead = afterLead.split('\n').find(line => line.trim() !== '').trim();
|
||||||
|
|
||||||
// Remove the lead from the main content
|
|
||||||
contentMarkdown = beforeLead + afterLead.replace(lead, '').trim();
|
contentMarkdown = beforeLead + afterLead.replace(lead, '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert markdown to HTML
|
|
||||||
const contentHtml = marked.parse(contentMarkdown);
|
const contentHtml = marked.parse(contentMarkdown);
|
||||||
|
|
||||||
return { contentHtml, lead };
|
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) {
|
function titleToSlug(title) {
|
||||||
return title
|
return title
|
||||||
.toLowerCase() // Convert to lowercase
|
.toLowerCase()
|
||||||
.replace(/[^a-z0-9\s-]/g, '') // Remove all non-alphanumeric characters except spaces and dashes
|
.replace(/[^a-z0-9\s-]/g, '')
|
||||||
.replace(/\s+/g, '-'); // Replace spaces with dashes
|
.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) {
|
function slugToTitle(slug) {
|
||||||
return slug.replace(/-/g, ' ');
|
return slug.replace(/-/g, ' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to load all blog posts with pagination support
|
// Function to load all blog posts with pagination and search support
|
||||||
function getAllBlogPosts(page = 1, postsPerPage = 5) {
|
function getAllBlogPosts(page = 1, postsPerPage = 5, searchQuery = '') {
|
||||||
const blogFiles = fs.readdirSync(path.join(__dirname, 'markdown')).filter(file => file.endsWith('.md'));
|
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) => {
|
blogFiles.sort((a, b) => {
|
||||||
const statA = fs.statSync(path.join(__dirname, 'markdown', a)).birthtime;
|
const statA = fs.statSync(path.join(__dirname, 'markdown', a)).birthtime;
|
||||||
const statB = fs.statSync(path.join(__dirname, 'markdown', b)).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 totalPosts = blogFiles.length;
|
||||||
const totalPages = Math.ceil(totalPosts / postsPerPage);
|
const totalPages = Math.ceil(totalPosts / postsPerPage);
|
||||||
const start = (page - 1) * postsPerPage;
|
const start = (page - 1) * postsPerPage;
|
||||||
@ -87,41 +82,35 @@ function getAllBlogPosts(page = 1, postsPerPage = 5) {
|
|||||||
const paginatedFiles = blogFiles.slice(start, end);
|
const paginatedFiles = blogFiles.slice(start, end);
|
||||||
|
|
||||||
const blogPosts = paginatedFiles.map(file => {
|
const blogPosts = paginatedFiles.map(file => {
|
||||||
const title = file.replace('.md', '').replace(/-/g, ' '); // Keep original casing for title
|
const title = file.replace('.md', '').replace(/-/g, ' ');
|
||||||
const slug = titleToSlug(title); // Convert title to slug (lowercase)
|
const slug = titleToSlug(title);
|
||||||
|
|
||||||
// Get the creation time of the markdown file
|
|
||||||
const stats = fs.statSync(path.join(__dirname, 'markdown', file));
|
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 {
|
return { title, slug, dateCreated };
|
||||||
title,
|
|
||||||
slug,
|
|
||||||
dateCreated, // Keep as Date object for sorting
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Return the paginated and sorted blog posts and the total pages
|
|
||||||
return { blogPosts, totalPages };
|
return { blogPosts, totalPages };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Home Route (Blog Home with Pagination)
|
// Home Route (Blog Home with Pagination and Search)
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
const page = parseInt(req.query.page) || 1;
|
const page = parseInt(req.query.page) || 1;
|
||||||
|
const searchQuery = req.query.search || '';
|
||||||
|
|
||||||
if (page < 1) {
|
if (page < 1) {
|
||||||
return res.redirect(req.hostname);
|
return res.redirect(req.hostname);
|
||||||
}
|
}
|
||||||
|
|
||||||
const postsPerPage = 5; // Set how many posts to display per page
|
const postsPerPage = 5;
|
||||||
|
const { blogPosts, totalPages } = getAllBlogPosts(page, postsPerPage, searchQuery);
|
||||||
const { blogPosts, totalPages } = getAllBlogPosts(page, postsPerPage);
|
|
||||||
|
|
||||||
res.render('index', {
|
res.render('index', {
|
||||||
title: 'Raven Scott Blog',
|
title: 'Raven Scott Blog',
|
||||||
blogPosts,
|
blogPosts,
|
||||||
currentPage: page,
|
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) => {
|
app.post('/contact', async (req, res) => {
|
||||||
const { name, email, subject, message, 'g-recaptcha-response': captchaToken } = req.body;
|
const { name, email, subject, message, 'g-recaptcha-response': captchaToken } = req.body;
|
||||||
|
|
||||||
// Validate form inputs (basic example)
|
|
||||||
if (!name || !email || !subject || !message) {
|
if (!name || !email || !subject || !message) {
|
||||||
return res.render('contact', { title: 'Contact Raven Scott', msg: 'All fields are required.' });
|
return res.render('contact', { title: 'Contact Raven Scott', msg: 'All fields are required.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the reCAPTCHA token
|
const captchaSecret = process.env.CAPTCHA_SECRET_KEY;
|
||||||
const captchaSecret = process.env.CAPTCHA_SECRET_KEY; // Your reCAPTCHA secret key
|
|
||||||
const captchaVerifyUrl = `https://www.google.com/recaptcha/api/siteverify?secret=${captchaSecret}&response=${captchaToken}`;
|
const captchaVerifyUrl = `https://www.google.com/recaptcha/api/siteverify?secret=${captchaSecret}&response=${captchaToken}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const captchaResponse = await axios.post(captchaVerifyUrl);
|
const captchaResponse = await axios.post(captchaVerifyUrl);
|
||||||
|
|
||||||
if (!captchaResponse.data.success) {
|
if (!captchaResponse.data.success) {
|
||||||
return res.render('contact', { title: 'Contact Raven Scott', msg: 'Captcha verification failed. Please try again.' });
|
return res.render('contact', { title: 'Contact Raven Scott', msg: 'Captcha verification failed. Please try again.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// CAPTCHA passed, proceed with sending email
|
|
||||||
const output = `
|
const output = `
|
||||||
<p>You have a new contact request from <strong>${name}</strong>.</p>
|
<p>You have a new contact request from <strong>${name}</strong>.</p>
|
||||||
<h3>Contact Details</h3>
|
<h3>Contact Details</h3>
|
||||||
@ -168,40 +153,31 @@ app.post('/contact', async (req, res) => {
|
|||||||
<p>${message}</p>
|
<p>${message}</p>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Set up Nodemailer transporter
|
|
||||||
let transporter = nodemailer.createTransport({
|
let transporter = nodemailer.createTransport({
|
||||||
host: process.env.SMTP_HOST,
|
host: process.env.SMTP_HOST,
|
||||||
port: process.env.SMTP_PORT,
|
port: process.env.SMTP_PORT,
|
||||||
secure: false, // true for 465, false for other ports
|
secure: false,
|
||||||
auth: {
|
auth: {
|
||||||
user: process.env.EMAIL_USER, // Email user from environment variables
|
user: process.env.EMAIL_USER,
|
||||||
pass: process.env.EMAIL_PASS, // Email password from environment variables
|
pass: process.env.EMAIL_PASS,
|
||||||
},
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false,
|
|
||||||
},
|
},
|
||||||
|
tls: { rejectUnauthorized: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up email options
|
|
||||||
let mailOptions = {
|
let mailOptions = {
|
||||||
from: `"${name}" <${process.env.RECEIVER_EMAIL}>`,
|
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,
|
subject: subject,
|
||||||
html: output,
|
html: output,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send email
|
|
||||||
transporter.sendMail(mailOptions, (error, info) => {
|
transporter.sendMail(mailOptions, (error, info) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error);
|
|
||||||
return res.render('contact', { title: 'Contact Raven Scott', msg: 'An error occurred. Please try again.' });
|
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) {
|
} 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.' });
|
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);
|
.find(file => titleToSlug(file.replace('.md', '')) === slug);
|
||||||
|
|
||||||
if (markdownFile) {
|
if (markdownFile) {
|
||||||
const originalTitle = markdownFile.replace('.md', ''); // Original title with casing
|
const originalTitle = markdownFile.replace('.md', '');
|
||||||
const blogPosts = getAllBlogPosts();
|
const blogPosts = getAllBlogPosts();
|
||||||
const { contentHtml, lead } = loadMarkdownWithLead(markdownFile);
|
const { contentHtml, lead } = loadMarkdownWithLead(markdownFile);
|
||||||
|
|
||||||
res.render('blog-post', {
|
res.render('blog-post', {
|
||||||
title: originalTitle, // Use the original title with casing
|
title: originalTitle,
|
||||||
content: contentHtml,
|
content: contentHtml,
|
||||||
lead: lead,
|
lead: lead,
|
||||||
blogPosts
|
blogPosts
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.redirect('/'); // Redirect to the home page if the blog post is not found
|
res.redirect('/');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sitemap Route
|
|
||||||
// Sitemap Route
|
// Sitemap Route
|
||||||
app.get('/sitemap.xml', (req, res) => {
|
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'))
|
const blogFiles = fs.readdirSync(path.join(__dirname, 'markdown'))
|
||||||
.filter(file => file.endsWith('.md'))
|
.filter(file => file.endsWith('.md'))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const statA = fs.statSync(path.join(__dirname, 'markdown', a)).birthtime;
|
const statA = fs.statSync(path.join(__dirname, 'markdown', a)).birthtime;
|
||||||
const statB = fs.statSync(path.join(__dirname, 'markdown', b)).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 = [
|
const staticUrls = [
|
||||||
{ url: `${process.env.HOST_URL}`, changefreq: 'weekly', priority: 1.0 },
|
{ 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}/about`, changefreq: 'monthly', priority: 0.8 },
|
||||||
{ url: `${process.env.HOST_URL}/contact`, 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 blogUrls = blogFiles.map(file => {
|
||||||
const title = file.replace('.md', '');
|
const title = file.replace('.md', '');
|
||||||
const slug = titleToSlug(title);
|
const slug = titleToSlug(title);
|
||||||
|
|
||||||
// Get the last modified date of the markdown file
|
|
||||||
const stats = fs.statSync(path.join(__dirname, 'markdown', file));
|
const stats = fs.statSync(path.join(__dirname, 'markdown', file));
|
||||||
const lastModifiedDate = format(new Date(stats.birthtime), 'yyyy-MM-dd');
|
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];
|
const urls = [...staticUrls, ...blogUrls];
|
||||||
|
|
||||||
// Generate the XML for the sitemap
|
|
||||||
let sitemap = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n`;
|
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 }) => {
|
urls.forEach(({ url, lastmod, changefreq, priority }) => {
|
||||||
sitemap += ` <url>\n`;
|
sitemap += ` <url>\n`;
|
||||||
@ -281,42 +250,33 @@ app.get('/sitemap.xml', (req, res) => {
|
|||||||
});
|
});
|
||||||
sitemap += `</urlset>`;
|
sitemap += `</urlset>`;
|
||||||
|
|
||||||
// Set the content type to XML and send the response
|
|
||||||
res.header('Content-Type', 'application/xml');
|
res.header('Content-Type', 'application/xml');
|
||||||
res.send(sitemap);
|
res.send(sitemap);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// RSS Feed Route
|
// RSS Feed Route
|
||||||
app.get('/rss', (req, res) => {
|
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'))
|
const blogFiles = fs.readdirSync(path.join(__dirname, 'markdown'))
|
||||||
.filter(file => file.endsWith('.md'))
|
.filter(file => file.endsWith('.md'))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const statA = fs.statSync(path.join(__dirname, 'markdown', a)).birthtime;
|
const statA = fs.statSync(path.join(__dirname, 'markdown', a)).birthtime;
|
||||||
const statB = fs.statSync(path.join(__dirname, 'markdown', b)).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 = `<?xml version="1.0" encoding="UTF-8" ?>\n<rss version="2.0">\n<channel>\n`;
|
let rssFeed = `<?xml version="1.0" encoding="UTF-8" ?>\n<rss version="2.0">\n<channel>\n`;
|
||||||
rssFeed += `<title>Raven Scott Blog</title>\n`;
|
rssFeed += `<title>Raven Scott Blog</title>\n`;
|
||||||
rssFeed += `<link>https://${hostname}</link>\n`;
|
rssFeed += `<link>https://${hostname}</link>\n`;
|
||||||
rssFeed += `<description>This is the RSS feed for Raven Scott's blog.</description>\n`;
|
rssFeed += `<description>This is the RSS feed for Raven Scott's blog.</description>\n`;
|
||||||
|
|
||||||
// Generate RSS items for each blog post
|
|
||||||
blogFiles.forEach(file => {
|
blogFiles.forEach(file => {
|
||||||
const title = file.replace('.md', '');
|
const title = file.replace('.md', '');
|
||||||
const slug = titleToSlug(title);
|
const slug = titleToSlug(title);
|
||||||
|
|
||||||
// Get the last modified date of the markdown file
|
|
||||||
const stats = fs.statSync(path.join(__dirname, 'markdown', file));
|
const stats = fs.statSync(path.join(__dirname, 'markdown', file));
|
||||||
const lastModifiedDate = new Date(stats.birthtime).toUTCString(); // Use UTC date for RSS
|
const lastModifiedDate = new Date(stats.birthtime).toUTCString();
|
||||||
|
|
||||||
// Load and parse markdown content to extract a lead or description
|
|
||||||
const { lead } = loadMarkdownWithLead(file);
|
const { lead } = loadMarkdownWithLead(file);
|
||||||
|
|
||||||
// RSS item for each post
|
|
||||||
rssFeed += `<item>\n`;
|
rssFeed += `<item>\n`;
|
||||||
rssFeed += `<title>${title}</title>\n`;
|
rssFeed += `<title>${title}</title>\n`;
|
||||||
rssFeed += `<link>${process.env.BLOG_URL}${slug}</link>\n`;
|
rssFeed += `<link>${process.env.BLOG_URL}${slug}</link>\n`;
|
||||||
@ -327,8 +287,6 @@ app.get('/rss', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
rssFeed += `</channel>\n</rss>`;
|
rssFeed += `</channel>\n</rss>`;
|
||||||
|
|
||||||
// Set content type to XML and send the RSS feed
|
|
||||||
res.header('Content-Type', 'application/rss+xml');
|
res.header('Content-Type', 'application/rss+xml');
|
||||||
res.send(rssFeed);
|
res.send(rssFeed);
|
||||||
});
|
});
|
||||||
@ -336,17 +294,13 @@ app.get('/rss', (req, res) => {
|
|||||||
// Global 404 handler for unmatched routes
|
// Global 404 handler for unmatched routes
|
||||||
app.use((req, res) => {
|
app.use((req, res) => {
|
||||||
if (req.hostname === 'blog.raven-scott.fyi') {
|
if (req.hostname === 'blog.raven-scott.fyi') {
|
||||||
// Redirect to the main domain
|
|
||||||
res.redirect(process.env.HOST_URL);
|
res.redirect(process.env.HOST_URL);
|
||||||
} else {
|
} else {
|
||||||
// Redirect to home page of the current domain
|
|
||||||
res.redirect('/');
|
res.redirect('/');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ================================
|
|
||||||
// Server Listening
|
// Server Listening
|
||||||
// ================================
|
|
||||||
const PORT = process.env.PORT || 8899;
|
const PORT = process.env.PORT || 8899;
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Server running on http://localhost:${PORT}`);
|
console.log(`Server running on http://localhost:${PORT}`);
|
||||||
|
@ -35,12 +35,21 @@
|
|||||||
<div class="container text-center">
|
<div class="container text-center">
|
||||||
<h1>Hello, my name is Raven Scott</h1>
|
<h1>Hello, my name is Raven Scott</h1>
|
||||||
<p class="lead">Where Technology Meets Creativity: Insights from a Linux Enthusiast</p>
|
<p class="lead">Where Technology Meets Creativity: Insights from a Linux Enthusiast</p>
|
||||||
|
<form action="/" method="get" class="mb-4">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" name="search" class="form-control" placeholder="Search blog posts..." value="<%= typeof searchQuery !== 'undefined' ? searchQuery : '' %>">
|
||||||
|
<button type="submit" class="btn btn-primary">Search</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- Search form -->
|
||||||
<section class="py-5">
|
<section class="py-5">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>Recent Posts</h2>
|
|
||||||
|
<!-- Blog post list -->
|
||||||
|
<h2><%= searchQuery ? `Search results for "${searchQuery}"` : 'Recent Posts' %></h2>
|
||||||
<ul class="list-group list-group-flush">
|
<ul class="list-group list-group-flush">
|
||||||
<% blogPosts.forEach(post => { %>
|
<% blogPosts.forEach(post => { %>
|
||||||
<li class="list-group-item d-flex justify-content-between align-items-center py-4">
|
<li class="list-group-item d-flex justify-content-between align-items-center py-4">
|
||||||
@ -58,9 +67,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Pagination controls -->
|
<!-- Pagination controls -->
|
||||||
<nav aria-label="Page navigation">
|
<nav aria-label="Page navigation">
|
||||||
<ul class="pagination justify-content-center mt-4">
|
<ul class="pagination justify-content-center mt-4">
|
||||||
|
Loading…
Reference in New Issue
Block a user