ravenscott-blog/markdown/Building a Feature-Rich Blog Platform with Node.js, Express, and Markdown.md
2024-09-26 04:06:02 -04:00

372 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- lead -->
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, 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, 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.
# Source
https://git.ssh.surf/snxraven/ravenscott-blog
## Why Node.js and Express?
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.
## 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:
```
/blog-platform
├── /markdown # Contains all blog posts written in Markdown
│ └── post-1.md # Example Markdown blog post
├── /public # Public assets (CSS, images, etc.)
│ └── /css
│ └── styles.css # Custom styles for the blog
├── /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 (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.)
└── README.md # Documentation
```
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.
### 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');
const path = require('path');
const fs = require('fs');
const { marked } = require('marked'); // For parsing Markdown files
const nodemailer = require('nodemailer'); // For sending emails from the contact form
const hljs = require('highlight.js'); // For syntax highlighting in code blocks
const axios = require('axios'); // For making HTTP requests, e.g., reCAPTCHA verification
const { format } = require('date-fns'); // For formatting dates in RSS feeds and sitemaps
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, 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).
- **date-fns**: A utility for formatting dates, which we use to ensure dates are correctly formatted in RSS feeds and sitemaps.
### 2. **Setting Up Middleware and Template Engine**
Express makes it easy to set up middleware, which is crucial for handling static assets (like CSS files), parsing form data, and rendering templates using a view engine.
#### EJS Templating Engine
We use **EJS** as the templating engine. This allows us to embed JavaScript logic directly within our HTML, making it possible to render dynamic content like blog posts and form submission results.
```javascript
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
```
This configuration tells Express to use the `views` folder for storing the HTML templates and `EJS` as the engine to render those templates.
#### Serving Static Files
Static files (like CSS and images) need to be served from the `/public` directory. This is where we store the CSS styles used to make the blog look visually appealing.
```javascript
app.use(express.static(path.join(__dirname, 'public')));
```
#### Parsing Form Data
When users submit the contact form, the form data is sent as **URL-encoded** data. To handle this, we use `express.urlencoded` middleware, which parses the form submissions and makes the data accessible via `req.body`.
```javascript
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.
### 1. **Setting Up `marked` and `highlight.js`**
To convert Markdown content to HTML, I used **Marked.js**, a fast and lightweight Markdown parser. Additionally, since many blog posts contain code snippets, **Highlight.js** is used to provide syntax highlighting for those snippets.
```javascript
marked.setOptions({
highlight: function (code, language) {
const validLanguage = hljs.getLanguage(language) ? language : 'plaintext';
return hljs.highlight(validLanguage, code).value;
}
});
```
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.
### 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
### 1. **Storing Blog Posts as Markdown Files**
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.
### 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 loadMarkdownWith
Lead(file) {
const markdownContent = fs.readFileSync(path.join(__dirname, 'markdown', file), 'utf-8');
let lead = '';
let contentMarkdown = markdownContent;
// Extract the lead section marked by `<-!-- lead -->`
const leadKeyword = '<-!-- lead -->';
if (contentMarkdown.includes(leadKeyword)) {
const [beforeLead, afterLead] = contentMarkdown.split(leadKeyword);
lead = afterLead.split('\n').find(line => line.trim() !== '').trim();
contentMarkdown = beforeLead + afterLead.replace(lead, '').trim();
}
const contentHtml = marked.parse(contentMarkdown);
return { contentHtml, lead };
}
```
### 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.
```javascript
app.get('/blog/:slug', (req, res) => {
const slug = req.params.slug;
const markdownFile = fs.readdirSync(path.join(__dirname, 'markdown'))
.find(file => titleToSlug(file.replace('.md', '')) === slug);
if (markdownFile) {
const originalTitle = markdownFile.replace('.md', '');
const { contentHtml, lead } = loadMarkdownWithLead(markdownFile);
res.render('blog-post', {
title: originalTitle,
content: contentHtml,
lead: lead
});
} else {
res.redirect('/');
}
});
```
### 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:
```javascript
function titleToSlug(title) {
return title.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '') // Remove non-alphanumeric characters
.replace(/\s+/g, '-'); // Replace spaces with dashes
}
```
This ensures that each blog post URL is clean and readable, with no special characters or extra whitespace.
## Adding Search Functionality
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, 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, ' ');
const slug = titleToSlug(title);
const stats = fs.statSync(path.join(__dirname, 'markdown', file));
const dateCreated = new Date(stats.birthtime);
return { title, slug, dateCreated };
});
return { blogPosts, totalPages };
}
```
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;
const { blogPosts, totalPages } = getAllBlogPosts(page, postsPerPage, searchQuery);
const noResults = blogPosts.length === 0; // Check if there are no results
res.render('index', {
title: `${process.env.OWNER_NAME}'s Blog`,
blogPosts,
currentPage: page,
totalPages,
searchQuery, // Pass search query to the view
noResults // Pass this flag to indicate no results found
});
});
```
In the `index.ejs` file, the search form dynamically updates the results, and the pagination controls help users navigate between pages.
```html
<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>
</form>
```
## Environment Variable Customization
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.
Heres a breakdown of the relevant `.env` variables:
```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
# CAPTCHA key for form verification
CAPTCHA_SECRET_KEY="KEYHERE"
# URL configuration
HOST_URL="https://yourtld.com" # Base host URL
BLOG_URL="https://blog.yourtld.com/" # Blog URL (with trailing slash)
# 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)
# 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
# Footer content
FOOTER_TAGLINE="Never Stop Learning" # Tagline for the footer
```
# 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.
**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.
**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.
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.
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.
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.
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.
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.
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.
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.
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.