update article

This commit is contained in:
Raven Scott 2024-09-26 03:51:45 -04:00
parent d3611e5640
commit 104f6b96d7

View File

@ -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, Ill walk you through every aspect of the platforms 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, Ill walk you through every aspect of the platforms 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.
Heres 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, lets 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
```
Heres 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.
Heres 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:
Ive 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 };
}
```
Heres whats 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 posts 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), theyre redirected to the homepage.
### 3. **Pagination Controls in EJS**
Heres 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 users 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 its validated and processed.
Heres 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 Googles 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. Markdowns 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 posts 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 its 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 dont 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.jss 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 applications 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 its 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.