diff --git a/markdown/Building a Feature-Rich Blog Platform with Node.js, Express, and Markdown.md b/markdown/Building a Feature-Rich Blog Platform with Node.js, Express, and Markdown.md index cca19d3..5738e55 100644 --- a/markdown/Building a Feature-Rich Blog Platform with Node.js, Express, and Markdown.md +++ b/markdown/Building a Feature-Rich Blog Platform with Node.js, Express, and Markdown.md @@ -1,23 +1,19 @@ - -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 - -``` - -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 -