Creating a Dynamic Blog with Node.js, Express, and EJS: A Comprehensive Guide - Part 2
- Ctrl Man
- Web Development , Backend , JavaScript
- 08 Jul, 2024
Creating a Dynamic Blog with Node.js, Express, and EJS: A Comprehensive Guide (Part 2)
Introduction
Welcome back to our two-part series on building a dynamic blog using Node.js, Express, and EJS. In Part 1, we covered setting up the project, creating a basic Express server, implementing EJS templates, creating routes for the blog, and implementing basic blog functionality. In this part, we’ll enhance the blog further by adding image uploads, styling the blog, implementing user authentication and comments, addressing security considerations, testing, and deploying the application.
Table of Contents for Part 2
- Adding Image Uploads
- Styling the Blog
- Adding Extra Features
- Security Considerations
- Testing
- Deployment
- Conclusion and Next Steps
Adding Image Uploads
Enhance the blog to include image uploads using multer
:
npm install multer
Updating app.js
to Include multer
// Update app.js to include multer
const multer = require('multer');
const upload = multer({ dest: 'public/uploads/' });
// Update create and edit post routes to handle file upload
router.post('/create', upload.single('image'), async (req, res) => {
try {
const { title, content, tags } = req.body;
const image = req.file ? `/uploads/${req.file.filename}` : null;
const result = await global.db.collection('posts').insertOne({
title,
content,
tags: tags.split(',').map(tag => tag.trim()),
image,
createdAt: new Date()
});
res.redirect(`/post/${result.insertedId}`);
} catch (error) {
console.error(error);
res.status(500).send('Error creating post');
}
});
// Handle file upload in the edit post route similarly
router.post('/edit/:id', upload.single('image'), async (req, res) => {
try {
const { title, content, tags } = req.body;
const image = req.file ? `/uploads/${req.file.filename}` : null;
const updateData = { title, content, tags: tags.split(',').map(tag => tag.trim()) };
if (image) updateData.image = image;
await global.db.collection('posts').updateOne(
{ _id: new ObjectId(req.params.id) },
{ $set: updateData }
);
res.redirect(`/post/${req.params.id}`);
} catch (error) {
console.error(error);
res.status(500).send('Error updating post');
}
});
Styling the Blog
Adding CSS Styles
Add a public/css/style.css
file to style the blog:
/* public/css/style.css */
body {
font-family: Arial, sans-serif;
}
.card {
margin-top: 20px;
}
.btn-primary {
background-color: #007bff;
border-color: #007bff;
}
.header, .footer {
background-color: #f8f9fa;
padding: 20px;
}
.container {
margin-top: 40px;
}
Adding Extra Features
User Authentication
Integrate user authentication using passport
and passport-local
:
npm install passport passport-local express-session bcrypt
Implementing User Authentication
In app.js
and routes/user.js
:
// app.js
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const session = require('express-session');
const bcrypt = require('bcrypt');
// Configure session
app.use(session({
secret: 'mySecret',
resave: false,
saveUninitialized: false
}));
// Configure passport
passport.use(new LocalStrategy(async (username, password, done) => {
try {
const user = await global.db.collection('users').findOne({ username });
if (!user || !await bcrypt.compare(password, user.password)) {
return done(null, false, { message: 'Invalid username or password' });
}
return done(null, user);
} catch (error) {
return done(error);
}
}));
passport.serializeUser((user, done) => {
done(null, user._id);
});
passport.deserializeUser(async (id, done) => {
try {
const user = await global.db.collection('users').findOne({ _id: new ObjectId(id) });
done(null, user);
} catch (error) {
done(error);
}
});
app.use(passport.initialize());
app.use(passport.session());
const userRoutes = require('./routes/user');
app.use('/', userRoutes);
// routes/user.js
const express = require('express');
const passport = require('passport');
const router = express.Router();
const bcrypt = require('bcrypt');
const saltRounds = 10;
router.get('/login', (req, res) => {
res.render('login', { title: 'Login' });
});
router.post('/login', passport.authenticate('local', {
successRedirect: '/',
failureRedirect: '/login'
}));
router.get('/register', (req, res) => {
res.render('register', { title: 'Register' });
});
router.post('/register', async (req, res) => {
try {
const { username, password } = req.body;
const hashedPassword = await bcrypt.hash(password, saltRounds);
await global.db.collection('users').insertOne({ username, password: hashedPassword });
res.redirect('/login');
} catch (error) {
console.error(error);
res.status(500).send('Error registering user');
}
});
router.get('/logout', (req, res) => {
req.logout();
res.redirect('/');
});
module.exports = router;
Comments
Add comments to blog posts:
// Update routes/blog.js
// Add comment to post
router.post('/post/:id/comment', async (req, res) => {
try {
const { comment } = req.body;
await global.db.collection('posts').updateOne(
{ _id: new ObjectId(req.params.id) },
{ $push: { comments: { text: comment, createdAt: new Date() } } }
);
res.redirect(`/post/${req.params.id}`);
} catch (error) {
console.error(error);
res.status(500).send('Error adding comment');
}
});
// Display comments in post view (post.ejs)
<% if (post.comments && post.comments.length > 0) { %>
<div class="comments">
<% post.comments.forEach(function(comment) { %>
<div class="comment">
<p><%= comment.text %></p>
<small><%= comment.createdAt.toDateString() %></small>
</div>
<% }); %>
</div>
<% } %>
<form action="/post/<%= post._id %>/comment" method="POST">
<div class="form-group">
<textarea name="comment" class="form-control" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Add Comment</button>
</form>
Security Considerations
Ensure passwords are stored securely:
Secure Passwords
// Update user registration to hash passwords
const bcrypt = require('bcrypt');
const saltRounds = 10;
router.post('/register', async (req, res) => {
try {
const { username, password } = req.body;
const hashedPassword = await bcrypt.hash(password, saltRounds);
await global.db.collection('users').insertOne({ username, password: hashedPassword });
res.redirect('/login');
} catch (error) {
console.error(error);
res.status(500).send('Error registering user');
}
});
// Update passport strategy to compare hashed passwords
passport.use(new LocalStrategy(async (username, password, done) => {
try {
const user = await global.db.collection('users').findOne({ username });
if (!user || !await bcrypt.compare(password, user.password)) {
return done(null, false, { message: 'Invalid username or password' });
}
return done(null, user);
} catch (error) {
return done(error);
}
}));
Testing
Integrate testing frameworks like Mocha
and Chai
:
npm install mocha chai supertest
Implementing Tests
Create a test
folder with blog.test.js
:
// test/blog.test.js
const request = require('supertest');
const app = require('../app'); // assuming app.js exports the express app
describe('GET /', function() {
it('responds with 200', function(done) {
request(app)
.get('/')
.expect(200, done);
});
});
describe('GET /post/:id', function() {
it('responds with 200', function(done) {
request(app)
.get('/post/60c72b2f9b1d8b3d4c8d889e') // Use a valid post ID from your database
.expect(200, done);
});
});
Deployment
Deploying to Heroku
Ensure you have a Procfile
and app.json
:
// Procfile
web: node app.js
// app.json
{
"name": "nodejs-express-blog",
"description": "A dynamic blog built with Node.js, Express, and EJS",
"scripts": {
"start": "node app.js"
}
}
Pushing to Heroku
heroku create
git push heroku main
heroku config:set MONGODB_URI=<your_mongodb_uri>
heroku open
Conclusion and Next Steps
In this two-part guide, we’ve covered the essentials of building a dynamic blog using Node.js, Express, and EJS. From setting up the project and creating routes to implementing user authentication, adding comments, and deploying the application, you now have a solid foundation to build upon.
For further improvements, consider adding features like social media integration, SEO enhancements, and performance optimizations. Happy coding!