forked from snxraven/ravenscott-blog
update article
This commit is contained in:
parent
d3611e5640
commit
104f6b96d7
@ -1,23 +1,19 @@
|
||||
<!-- lead -->
|
||||
How I built this blog, A Deep Dive into Modern Web Development.
|
||||
How I Built This Blog: A Deep Dive into Modern Web Development
|
||||
|
||||
A blog is one of the most powerful tools for sharing information, building authority, and engaging with an audience. When I decided to build a blog platform using **Node.js**, I wanted to go beyond the typical setup. I aimed for a feature-rich platform that dynamically serves content from Markdown files, supports pagination, integrates syntax highlighting for code snippets, offers a functional contact form with **reCAPTCHA** validation, and generates **RSS** feeds and **sitemaps** for better SEO.
|
||||
A blog is one of the most powerful tools for sharing information, building authority, and engaging with an audience. When I decided to build a blog platform using **Node.js**, I wanted to go beyond the typical setup. I aimed for a feature-rich platform that dynamically serves content from Markdown files, supports pagination, integrates syntax highlighting for code snippets, offers a functional contact form with **reCAPTCHA** validation, generates **RSS** feeds and **sitemaps** for better SEO, and allows for customized pages like "About Me" to be loaded directly from Markdown files.
|
||||
|
||||
In this in-depth technical breakdown, I’ll walk you through every aspect of the platform’s architecture and code, discussing why I chose each technology and how the different parts work together. If you're looking to create your own blog platform or simply want to dive deeper into building dynamic web applications with Node.js, this post will cover everything in great detail.
|
||||
In this in-depth technical breakdown, I’ll walk you through every aspect of the platform’s architecture and code, explaining why I chose each technology and how the different parts work together. If you're looking to create your own blog platform or simply want to dive deeper into building dynamic web applications with **Node.js**, this post will cover everything in great detail.
|
||||
|
||||
## Why Node.js and Express?
|
||||
|
||||
Before we get into the nitty-gritty, let's talk about the choice of technologies. I chose **Node.js** as the runtime because of its event-driven, non-blocking I/O model, which is great for building scalable and performant web applications. **Express.js**, a minimalist web framework for Node, simplifies the process of setting up a web server, routing requests, and serving static files.
|
||||
Before we get into the technical details, let's talk about the choice of technologies. I chose **Node.js** as the runtime because of its event-driven, non-blocking I/O model, which is great for building scalable and performant web applications. **Express.js**, a minimalist web framework for Node, simplifies the process of setting up a web server, routing requests, and serving static files.
|
||||
|
||||
Here’s why these choices make sense for this project:
|
||||
|
||||
- **Node.js**: Handles high-concurrency applications well, meaning it can efficiently serve multiple blog readers without performance bottlenecks.
|
||||
- **Express.js**: Provides a straightforward way to build a RESTful architecture for managing routes, handling form submissions, and rendering views dynamically.
|
||||
|
||||
# Source
|
||||
|
||||
## https://git.ssh.surf/snxraven/ravenscott-blog
|
||||
|
||||
## Folder Structure: Organizing the Blog Platform
|
||||
|
||||
One of the first things you need to think about when building a project is its structure. Here's a breakdown of the folder structure I used for this blog platform:
|
||||
@ -35,9 +31,12 @@ One of the first things you need to think about when building a project is its s
|
||||
├── /views # EJS templates (HTML views rendered by the server)
|
||||
│ ├── index.ejs # Homepage template showing a list of blog posts
|
||||
│ ├── blog-post.ejs # Template for individual blog posts
|
||||
│ ├── about.ejs # "About Me" page
|
||||
│ ├── about.ejs # "About Me" page (loaded from markdown)
|
||||
│ └── contact.ejs # Contact form page
|
||||
│
|
||||
├── /me # Personal markdown files (like About Me)
|
||||
│ └── about.md # Markdown file for the "About Me" page
|
||||
│
|
||||
├── app.js # Main server file, handles all backend logic
|
||||
├── package.json # Project dependencies and scripts
|
||||
├── .env # Environment variables (API keys, credentials, etc.)
|
||||
@ -45,13 +44,13 @@ One of the first things you need to think about when building a project is its s
|
||||
```
|
||||
|
||||
This structure provides a clear separation of concerns:
|
||||
|
||||
- **Markdown** files are stored in their own directory.
|
||||
- **Public** assets (CSS, images) are isolated for easy reference.
|
||||
- **Views** are where EJS templates are stored, allowing us to easily manage the HTML structure of each page.
|
||||
- **me** contains personal information like the **about.md** file, which gets rendered dynamically for the "About Me" page.
|
||||
- **app.js** acts as the control center, handling the routes, form submissions, and the logic for rendering content.
|
||||
|
||||
|
||||
|
||||
## Setting Up the Express Server
|
||||
|
||||
The core of the application is the **Express.js** server, which powers the entire backend. In `app.js`, we initialize Express, set up the middleware, and define the routes. But before we get into the route handling, let’s break down the middleware and configuration settings we used.
|
||||
@ -59,6 +58,7 @@ The core of the application is the **Express.js** server, which powers the entir
|
||||
### 1. **Loading Dependencies**
|
||||
|
||||
Here are the key dependencies we load at the top of the file:
|
||||
|
||||
```javascript
|
||||
require('dotenv').config(); // Load environment variables from .env
|
||||
const express = require('express');
|
||||
@ -74,9 +74,10 @@ const app = express(); // Initialize Express
|
||||
```
|
||||
|
||||
Here’s what each dependency does:
|
||||
|
||||
- **dotenv**: Loads environment variables from a `.env` file, which we use to store sensitive information like API keys and email credentials.
|
||||
- **path** and **fs**: Standard Node.js modules that help us work with file paths and file systems. We use these to read Markdown files and serve static assets.
|
||||
- **marked**: A Markdown parser that converts Markdown syntax into HTML, which allows us to write blog posts using a simple syntax.
|
||||
- **marked**: A Markdown parser that converts Markdown syntax into HTML, allowing us to write blog posts using a simple syntax.
|
||||
- **nodemailer**: Handles sending emails when users submit the contact form.
|
||||
- **highlight.js**: Provides syntax highlighting for any code blocks in the blog posts. This is essential for making technical posts more readable.
|
||||
- **axios**: Used for making external HTTP requests (e.g., verifying the Google reCAPTCHA response).
|
||||
@ -113,8 +114,6 @@ When users submit the contact form, the form data is sent as **URL-encoded** dat
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Markdown Parsing with Syntax Highlighting
|
||||
|
||||
One of the primary features of this blog platform is that it allows you to write blog posts using **Markdown**. Markdown is a simple markup language that converts easily to HTML and is especially popular among developers because of its lightweight syntax for writing formatted text.
|
||||
@ -132,11 +131,31 @@ marked.setOptions({
|
||||
});
|
||||
```
|
||||
|
||||
- **highlight**: The function accepts the code block and its language (e.g., `javascript`, `python`) and passes it to `highlight.js` for highlighting. If the language is invalid, we default to plain text.
|
||||
This makes code blocks in blog posts more readable by colorizing keywords, variables, and other syntax elements, which improves the user experience, especially for technical blogs.
|
||||
|
||||
This makes code blocks in blog posts look more readable by colorizing keywords, variables, and other syntax elements, which improves the user experience, especially for technical blogs.
|
||||
### 2. **Loading the 'About Me' Page from Markdown**
|
||||
|
||||
To create a more personalized "About Me" page, I stored the content in a Markdown file (`me/about.md`) and dynamically rendered it with Express. Here's how we load the `about.md` file and render it using **EJS**:
|
||||
|
||||
```javascript
|
||||
app.get('/about', (req, res) => {
|
||||
const aboutMarkdownFile = path.join(__dirname, 'me', 'about.md');
|
||||
|
||||
// Read the markdown file and convert it 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}`,
|
||||
content: aboutContentHtml
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Blog Post Storage and Rendering
|
||||
|
||||
@ -144,16 +163,14 @@ This makes code blocks in blog posts look more readable by colorizing keywords,
|
||||
|
||||
Instead of storing blog posts in a database, this platform uses a simpler approach: each blog post is a `.md` file stored in the `/markdown` directory. This approach is not only easier to manage, but it also gives writers the flexibility to create and update posts using any text editor.
|
||||
|
||||
Here’s what a sample Markdown file (`post-1.md`) might look like:
|
||||
|
||||
- **Lead Section**: The comment `<-!-- lead -->` marks the start of the lead section. This is a short summary or introduction that appears on the homepage or in the RSS feed.
|
||||
|
||||
### 2. **Rendering Markdown as HTML**
|
||||
|
||||
To render the Markdown content as HTML on the frontend, we define a function `loadMarkdownWithLead` that reads the Markdown file, parses it, and extracts the lead section if available.
|
||||
|
||||
```js
|
||||
function loadMarkdownWithLead(file) {
|
||||
function loadMarkdownWith
|
||||
|
||||
Lead(file) {
|
||||
const markdownContent = fs.readFileSync(path.join(__dirname, 'markdown', file), 'utf-8');
|
||||
|
||||
let lead = '';
|
||||
@ -164,9 +181,7 @@ function loadMarkdownWithLead(file) {
|
||||
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();
|
||||
contentMarkdown = beforeLead + afterLead.replace(lead, '').trim();
|
||||
}
|
||||
|
||||
const contentHtml = marked.parse(contentMarkdown);
|
||||
@ -174,15 +189,10 @@ trim();
|
||||
}
|
||||
```
|
||||
|
||||
- **Markdown Parsing**: This function reads the Markdown file using `fs.readFileSync`, extracts the lead if present, and passes the remaining content to `marked` for parsing into HTML.
|
||||
- **Lead Section**: We use a special comment tag `<-!-- lead -->` to mark the start of the lead section. The `loadMarkdownWithLead` function extracts this section and removes it from the rest of the content, allowing us to display it separately on the homepage or in feeds.
|
||||
|
||||
### 3. **Dynamically Rendering Blog Posts**
|
||||
|
||||
For each blog post, we generate a URL based on its title (converted to a slug format). The user can access a blog post by navigating to `/blog/{slug}`, where `{slug}` is the URL-friendly version of the title.
|
||||
|
||||
#### Blog Post Route:
|
||||
|
||||
```javascript
|
||||
app.get('/blog/:slug', (req, res) => {
|
||||
const slug = req.params.slug;
|
||||
@ -190,7 +200,7 @@ app.get('/blog/:slug', (req, res) => {
|
||||
.find(file => titleToSlug(file.replace('.md', '')) === slug);
|
||||
|
||||
if (markdownFile) {
|
||||
const originalTitle = markdownFile.replace('.md', ''); // Retain original title case
|
||||
const originalTitle = markdownFile.replace('.md', '');
|
||||
const { contentHtml, lead } = loadMarkdownWithLead(markdownFile);
|
||||
|
||||
res.render('blog-post', {
|
||||
@ -199,15 +209,11 @@ app.get('/blog/:slug', (req, res) => {
|
||||
lead: lead
|
||||
});
|
||||
} else {
|
||||
res.redirect('/'); // If the post doesn't exist, redirect to the homepage
|
||||
res.redirect('/');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
This route handles the logic for displaying a single blog post:
|
||||
- We take the `slug` from the URL, convert it into a filename, and attempt to find the corresponding Markdown file.
|
||||
- If the file exists, we render it using the `blog-post.ejs` template, passing the title, lead, and content as variables.
|
||||
|
||||
### 4. **Slug Generation for Blog Posts**
|
||||
|
||||
The title of each post is converted into a URL-friendly slug, which is used in the post's URL. Here's the utility function for converting a title into a slug:
|
||||
@ -220,328 +226,143 @@ function titleToSlug(title) {
|
||||
}
|
||||
```
|
||||
|
||||
This function ensures that each blog post URL is clean and readable, with no special characters or extra whitespace.
|
||||
This ensures that each blog post URL is clean and readable, with no special characters or extra whitespace.
|
||||
|
||||
## Adding Search Functionality
|
||||
|
||||
|
||||
## Pagination: Listing Blog Posts with Page Navigation
|
||||
|
||||
To improve the user experience, we add **pagination** to the homepage. Rather than displaying all blog posts at once, we only show a limited number of posts per page (e.g., 5 posts per page). Users can navigate between pages using pagination controls.
|
||||
|
||||
### 1. **Paginating Blog Posts**
|
||||
|
||||
To implement pagination, we first define a function `getAllBlogPosts` that reads all the Markdown files, sorts them by date, and slices the list based on the current page:
|
||||
I’ve implemented a search feature that allows users to search for blog posts by title. The search functionality reads through all the Markdown filenames and returns posts that match the search query.
|
||||
|
||||
```javascript
|
||||
function getAllBlogPosts(page = 1, postsPerPage = 5) {
|
||||
const blogFiles = fs.readdirSync(path.join(__dirname, 'markdown')).filter(file => file.endsWith('.md'));
|
||||
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, ' '); // Keep original casing for title
|
||||
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); // Get file creation date
|
||||
const formattedDate = dateCreated.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
const dateCreated = new Date(stats.birthtime);
|
||||
|
||||
return {
|
||||
title,
|
||||
slug,
|
||||
date: formattedDate
|
||||
};
|
||||
return { title, slug, dateCreated };
|
||||
});
|
||||
|
||||
return { blogPosts, totalPages };
|
||||
}
|
||||
```
|
||||
|
||||
Here’s what’s happening:
|
||||
- We load all Markdown files from the `/markdown` directory.
|
||||
- **Pagination logic**: We calculate how many posts to display per page. For example, if there are 10 posts and the user is on page 2, we slice the array of blog posts to show posts 6 through 10.
|
||||
- Each blog post’s title, slug, and date are extracted and returned as part of the `blogPosts` array, which is then rendered on the homepage.
|
||||
|
||||
### 2. **Rendering the Homepage with Pagination**
|
||||
|
||||
The homepage route loads the blog posts for the current page and renders them in the `index.ejs` template. Pagination controls are also added to navigate between pages.
|
||||
The search query is passed through the route and displayed dynamically on the homepage with the search results.
|
||||
|
||||
```javascript
|
||||
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);
|
||||
|
||||
const noResults = blogPosts.length === 0; // Check if there are no results
|
||||
|
||||
res.render('index', {
|
||||
title: 'Raven Scott Blog',
|
||||
title: `${process.env.OWNER_NAME}'s Blog`,
|
||||
blogPosts,
|
||||
currentPage: page,
|
||||
totalPages
|
||||
totalPages,
|
||||
searchQuery, // Pass search query to the view
|
||||
noResults // Pass this flag to indicate no results found
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- **Current Page Handling**: We extract the current page number from the query parameters (`req.query.page`). If no page is specified, we default to page 1.
|
||||
- **Redirection**: If the user tries to navigate to an invalid page (e.g., page 0 or below), they’re redirected to the homepage.
|
||||
|
||||
### 3. **Pagination Controls in EJS**
|
||||
|
||||
Here’s how we render the pagination controls in the `index.ejs` template:
|
||||
In the `index.ejs` file, the search form dynamically updates the results, and the pagination controls help users navigate between pages.
|
||||
|
||||
```html
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center mt-4">
|
||||
<% if (currentPage > 1) { %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=<%= currentPage - 1 %>">Previous</a>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% for (let i = 1; i <= totalPages; i++) { %>
|
||||
<li class="page-item <%= currentPage === i ? 'active' : '' %>">
|
||||
<a class="page-link" href="?page=<%= i %>"><%= i %></a>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% if (currentPage < totalPages) { %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=<%= currentPage + 1 %>">Next</a>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</nav>
|
||||
```
|
||||
|
||||
This generates pagination links dynamically based on the current page and the total number of pages. The current page is highlighted, and users can navigate to the previous or next page easily.
|
||||
|
||||
|
||||
|
||||
## Contact Form with Google reCAPTCHA
|
||||
|
||||
One of the essential features of this platform is the **contact form**, where users can submit messages that get sent to the admin via email. To prevent spam and bot submissions, we integrate **Google reCAPTCHA** for human verification.
|
||||
|
||||
### 1. **Creating the Contact Form**
|
||||
|
||||
The contact form is rendered in the `contact.ejs` template. It includes fields for the user’s name, email, subject, and message, as well as a Google reCAPTCHA widget.
|
||||
|
||||
```html
|
||||
<form action="/contact" method="POST" class="mt-4">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Your Name<span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="name" name="name" required>
|
||||
<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 : '' %>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email Address<span class="text-danger">*</span></label>
|
||||
<input type="email" class="form-control" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="subject" class="form-label">Subject<span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="subject" name="subject" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="message" class="form-label">Your Message<span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="message" name="message" rows="6" required></textarea>
|
||||
</div>
|
||||
<div class="g-recaptcha" data-sitekey="<%= process.env.CAPTCHA_SITE_KEY %>"></div>
|
||||
<button type="submit" class="btn btn-primary">Send Message</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
The reCAPTCHA widget is included as a `<div>` element with the `data-sitekey` attribute. This ensures that the user needs to verify they are human before submitting the form.
|
||||
## Environment Variable Customization
|
||||
|
||||
### 2. **Form Submission Handling and reCAPTCHA Validation**
|
||||
The `.env` file contains all the configuration settings for the site, making it easy to change things like the owner's name, email settings, and URLs without modifying the code.
|
||||
|
||||
When the form is submitted, the data (along with the reCAPTCHA response token) is sent to the server, where it’s validated and processed.
|
||||
Here’s a breakdown of the relevant `.env` variables:
|
||||
|
||||
```javascript
|
||||
app.post('/contact', async (req, res) => {
|
||||
const { name, email, subject, message, 'g-recaptcha-response': captchaToken
|
||||
```env
|
||||
# SMTP configuration for sending emails
|
||||
SMTP_HOST=us2.smtp.yourtld.com # SMTP server host
|
||||
SMTP_PORT=587 # SMTP server port
|
||||
EMAIL_USER=user@yourtld.com # Email address used for SMTP authentication
|
||||
EMAIL_PASS="ComplexPass" # Password for the SMTP user
|
||||
RECEIVER_EMAIL=youremail@yourtld.com # Default receiver email for outgoing messages
|
||||
|
||||
} = req.body;
|
||||
# CAPTCHA key for form verification
|
||||
CAPTCHA_SECRET_KEY="KEYHERE"
|
||||
|
||||
if (!name || !email || !subject || !message) {
|
||||
return res.render('contact', { msg: 'All fields are required.' });
|
||||
}
|
||||
# URL configuration
|
||||
HOST_URL="https://yourtld.com" # Base host URL
|
||||
BLOG_URL="https://blog.yourtld.com/" # Blog URL (with trailing slash)
|
||||
|
||||
// Verify the reCAPTCHA token with Google
|
||||
const captchaSecret = process.env.CAPTCHA_SECRET_KEY;
|
||||
const captchaVerifyUrl = `https://www.google.com/recaptcha/api/siteverify?secret=${captchaSecret}&response=${captchaToken}`;
|
||||
# Website branding
|
||||
SITE_NAME="Your Blog Title" # Title used in the website's navbar
|
||||
OWNER_NAME="Your Name" # Name of the website's owner (you)
|
||||
|
||||
try {
|
||||
const captchaResponse = await axios.post(captchaVerifyUrl);
|
||||
if (!captchaResponse.data.success) {
|
||||
return res.render('contact', { msg: 'Captcha verification failed.' });
|
||||
}
|
||||
# Front page content
|
||||
FRONT_PAGE_TITLE="Hello, my name is Your Name" # Main heading on the homepage
|
||||
FRONT_PAGE_LEAD="Where Technology Meets Creativity: Insights from a Linux Enthusiast" # Lead text on the homepage
|
||||
|
||||
// If CAPTCHA is verified, send email
|
||||
const output = `
|
||||
<p>You have a new contact request from <strong>${name}</strong>.</p>
|
||||
<h3>Contact Details</h3>
|
||||
<ul>
|
||||
<li>Name: ${name}</li>
|
||||
<li>Email: ${email}</li>
|
||||
<li>Subject: ${subject}</li>
|
||||
</ul>
|
||||
<h3>Message</h3>
|
||||
<p>${message}</p>
|
||||
`;
|
||||
|
||||
// Send email using nodemailer
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: process.env.SMTP_PORT,
|
||||
auth: {
|
||||
user: process.env.EMAIL_USER,
|
||||
pass: process.env.EMAIL_PASS
|
||||
}
|
||||
});
|
||||
|
||||
const mailOptions = {
|
||||
from: email,
|
||||
to: process.env.RECEIVER_EMAIL,
|
||||
subject,
|
||||
html: output
|
||||
};
|
||||
|
||||
transporter.sendMail(mailOptions, (error, info) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return res.render('contact', { msg: 'An error occurred. Please try again.' });
|
||||
} else {
|
||||
console.log('Message sent: %s', info.messageId);
|
||||
return res.render('contact', { msg: 'Your message has been sent successfully!' });
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error verifying reCAPTCHA:', error);
|
||||
return res.render('contact', { msg: 'Captcha verification failed. Please try again.' });
|
||||
}
|
||||
});
|
||||
# Footer content
|
||||
FOOTER_TAGLINE="Never Stop Learning" # Tagline for the footer
|
||||
```
|
||||
|
||||
- **Basic Validation**: We check that all required fields (name, email, subject, message) are filled in before proceeding.
|
||||
- **reCAPTCHA Verification**: The reCAPTCHA token is sent to Google’s API for verification. If the response is valid, the form submission proceeds. Otherwise, an error message is displayed to the user.
|
||||
- **Sending Email**: Once reCAPTCHA is verified, the form data is formatted into an HTML message and sent to the site administrator via email using **Nodemailer**.
|
||||
# Final Thoughts
|
||||
|
||||
By leveraging **Node.js**, **Express**, **EJS**, and **Markdown**, this blog platform demonstrates how you can combine modern, lightweight technologies to build a dynamic, feature-rich website that is both scalable and easy to maintain. These technologies work together to offer a seamless development experience, allowing you to focus on creating content and functionality rather than worrying about performance bottlenecks or complex configurations.
|
||||
|
||||
**Node.js** is renowned for its event-driven, non-blocking architecture, making it perfect for real-time applications and websites that require high concurrency. It allows the platform to handle multiple users and requests simultaneously without compromising performance. This is crucial for blogs or websites with growing traffic, where responsiveness and speed are essential to user experience. The efficiency of Node.js, along with its ability to unify backend and frontend development through JavaScript, creates a cohesive environment that is both efficient and developer-friendly. Whether scaling the application for higher traffic or deploying updates quickly, Node.js provides a fast, reliable runtime.
|
||||
|
||||
## Generating an RSS Feed
|
||||
**Express.js** simplifies the challenges of building a backend server. Its minimalist design allows for easy routing, middleware configuration, and management of HTTP requests. In this blog platform, Express plays a key role in routing different parts of the site, such as serving static assets, rendering dynamic content with EJS, handling form submissions, and integrating security features like reCAPTCHA in the contact form. Express is designed to be flexible and extendable, allowing you to integrate additional functionality like authentication, session management, or third-party APIs with minimal effort. Its built-in support for middleware also enables developers to easily add features or customize existing ones, making the platform adaptable to evolving needs.
|
||||
|
||||
For users who want to subscribe to the blog, I added an **RSS feed** feature. RSS feeds allow users to get updates from the blog using feed readers. Each time a new post is published, it automatically appears in the feed.
|
||||
**EJS (Embedded JavaScript Templates)** is used to render dynamic content within HTML, making it easy to inject variables and logic directly into views. In this project, EJS powers the dynamic rendering of blog posts, search results, pagination, and custom pages like the "About Me" section. By allowing us to integrate JavaScript logic directly into HTML templates, EJS enables a more interactive and personalized user experience. It also supports the reuse of templates, which helps to keep the code clean and modular. The familiarity of EJS with standard HTML means developers can quickly get up to speed without learning an entirely new templating language.
|
||||
|
||||
### RSS Feed Route
|
||||
The use of **Markdown** as the primary format for content creation offers simplicity and flexibility. Storing blog posts in Markdown files removes the need for a complex database, making the platform lightweight and easy to manage. Markdown’s intuitive syntax allows content creators to focus on writing, while the platform automatically handles formatting and presentation. When paired with tools like **Marked.js**, Markdown becomes even more powerful, as it allows for easy conversion from plain text into rich HTML. This setup is particularly useful for technical blogs, where code snippets are often embedded. By integrating **highlight.js**, the platform ensures that code blocks are both functional and beautifully presented, making the reading experience more enjoyable and accessible for developers and technical audiences.
|
||||
|
||||
The route `/rss` generates the RSS feed in XML format, listing all the recent blog posts. The feed includes the post’s title, description (lead), link, and publication date.
|
||||
This combination of technologies unlocks several powerful features that enhance both the user experience and the development process. With **dynamic content rendering**, the platform efficiently serves blog posts, handles search queries, and manages pagination on the fly. The content is written and stored in Markdown files, but it’s transformed into fully styled HTML at the moment of request, allowing for quick updates and modifications. This approach not only makes content management easier but also ensures that users always see the most up-to-date version of the blog without requiring database queries or complex caching mechanisms.
|
||||
|
||||
```javascript
|
||||
app.get('/rss', (req, res) => {
|
||||
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;
|
||||
});
|
||||
The **flexibility and extendability** of this platform are key advantages. Whether you want to add new features, such as a gallery or portfolio section, or integrate external services like a newsletter or analytics platform, the modular structure of Express and the use of EJS templates make this process straightforward. Adding a new feature is as simple as creating a new Markdown file and a corresponding EJS template, enabling rapid development and easy customization. This makes the platform ideal for developers who want to scale or expand their site over time without worrying about technical debt.
|
||||
|
||||
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 += `<link>https://${req.hostname}</link>\n`;
|
||||
rssFeed += `<description>This is the RSS feed for Raven Scott's blog.</description>\n`;
|
||||
A key principle of this platform is **separation of concerns**, which ensures that the content, logic, and presentation are kept distinct. Blog posts are stored as Markdown files, static assets like CSS and images are kept in their own directory, and the logic for handling routes and rendering views is managed in the Express app. This makes the platform highly maintainable, as changes to one part of the system don’t affect other parts. For instance, you can easily update the styling of the blog without changing the logic that handles blog posts or search functionality.
|
||||
|
||||
blogFiles.forEach(file => {
|
||||
const { lead } = loadMarkdownWithLead(file);
|
||||
const title = file.replace('.md', '');
|
||||
const slug = titleToSlug(title);
|
||||
const stats = fs.statSync(path.join(__dirname, 'markdown', file));
|
||||
const lastModifiedDate = new Date(stats.birthtime).toUTCString();
|
||||
Furthermore, **performance and security** are built into the platform from the start. Node.js’s asynchronous, non-blocking architecture ensures that the platform can handle high levels of concurrency with minimal latency. Meanwhile, Express allows for easy integration of security features like **reCAPTCHA**, ensuring that spam submissions are minimized. The use of environment variables stored in a `.env` file means sensitive information, like email credentials and API keys, is kept secure and easily configurable. This approach not only enhances security but also simplifies the deployment process, as configurations can be adjusted without changing the codebase.
|
||||
|
||||
rssFeed += `<item>\n`;
|
||||
rssFeed += `<title>${title}</title>\n`;
|
||||
rssFeed += `<link>https://${req.hostname}/blog/${slug}</link>\n`;
|
||||
rssFeed += `<description>${lead || 'Read the full post on the blog.'}</description>\n`;
|
||||
rssFeed += `<pubDate>${lastModifiedDate}</pubDate>\n`;
|
||||
rssFeed += `<guid>https://${req.hostname}/blog/${slug}</guid>\n`;
|
||||
rssFeed += `</item>\n`;
|
||||
});
|
||||
One of the standout features of this platform is its **search functionality**. Users can easily search for blog posts by title, with results rendered dynamically based on the query. This is made possible through the flexible routing capabilities of Express, combined with the simplicity of searching through Markdown filenames. The integration of search functionality elevates the user experience, providing quick access to relevant content while maintaining a responsive interface.
|
||||
|
||||
rssFeed += `</channel>\n</rss>`;
|
||||
Finally, the **environmental customizations** enabled by the `.env` file make the platform incredibly versatile. The `.env` file stores crucial configuration details such as email server settings, CAPTCHA keys, and URLs, allowing these values to be updated without modifying the application’s source code. This separation of configuration and logic streamlines deployment and maintenance, especially when migrating the platform to different environments or adjusting for production and development needs. By externalizing configuration, the platform can be easily adapted to different hosting environments, whether it’s deployed on a local server, a cloud service, or a dedicated VPS.
|
||||
|
||||
res.header('Content-Type', 'application/rss+xml');
|
||||
res.send(rssFeed);
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Generating a Sitemap for SEO
|
||||
|
||||
To help search engines like Google crawl and index the blog, I implemented a **sitemap**. The sitemap lists all the URLs on the site and includes metadata about each page (e.g., when it was last modified, how frequently it changes).
|
||||
|
||||
### Sitemap Route
|
||||
|
||||
The `/sitemap.xml` route generates an XML sitemap listing all the static and dynamic pages (like blog posts):
|
||||
|
||||
```javascript
|
||||
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'));
|
||||
|
||||
const staticUrls = [
|
||||
{ url: '/', changefreq: 'weekly', priority: 1.0 },
|
||||
{ url: '/about', changefreq: 'monthly', priority: 0.8 },
|
||||
{ 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: `/blog/${slug}`,
|
||||
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`;
|
||||
sitemap += ` <loc>https://${hostname}${url}</loc>\n`;
|
||||
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);
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
|
||||
## My thoughts
|
||||
This blog platform demonstrates how you can combine several modern technologies to build a feature-rich website. The use of **Node.js** and **Express** offers a lightweight, fast, and scalable solution for serving content. By storing blog posts as Markdown files, we avoid the complexity of using a database while still providing dynamic functionality.
|
||||
In conclusion, this blog platform showcases how **Node.js**, **Express**, **EJS**, and **Markdown** can be combined to create a robust, feature-rich website that is highly adaptable to various content needs. From dynamic blog posts to customizable pages like "About Me," to integrated search functionality and secure contact forms, this platform provides a flexible and efficient solution for content creators, developers, and businesses alike. Its scalability, maintainability, and performance make it a perfect choice for anyone looking to build a modern, high-performance blog or content management system.
|
Loading…
Reference in New Issue
Block a user