Implement Comments Section in MERN Blogs and News Website
Last Updated :
14 Nov, 2024
In the digital age, the way we consume news and stories has drastically changed. Blogs and news websites have become the go-to sources for information, offering readers a vast array of content at their fingertips. However, what truly sets these platforms apart from traditional media is the ability for readers to engage directly with the content through the comments section.
The comments section is often viewed as an extension of the article itself. It serves as a space where readers can share their thoughts, ask questions, and discuss their perspectives. This interaction transforms a static piece of content into a living, evolving conversation.
For many readers, the comments section is just as important as the article. It provides a platform for them to express their opinions, engage in discussions, and even challenge the views presented. This dialogue can lead to a richer understanding of the topic, as readers learn from each other’s insights and experiences.
Enhancing the Reader Experience
The comments section also enhances the reader experience by providing different perspectives on the same topic. Articles are often written from a specific viewpoint, but the comments section opens the floor to a variety of opinions and interpretations. This diversity of thought can deepen the reader’s understanding and spark further curiosity about the topic.
Additionally, the comments section allows readers to seek clarification on points that may be unclear in the article. Other readers or even the author can respond, providing additional information and context that enriches the original content.
While the comments section offers numerous benefits, it’s not without its challenges. Moderating the comments to ensure that discussions remain respectful and on-topic is crucial. Content creators need to strike a balance between allowing free expression and maintaining a positive environment.
To address this, many websites implement moderation tools or guidelines for commenters. These measures help to filter out spam, hate speech, and irrelevant content, ensuring that the comments section remains a productive space for discussion.
Approach to Implement Comment Section in Blog and News Website
Backend:
- Validate the author’s ID, find the article by articleId, add the comment to the article’s comments array, save the updated article, and handle errors appropriately.
- Retrieve the article by articleId, populate author details in comments, return the comments array if found, or respond with a 404 error if not, and manage potential errors.
- Locate the article by articleId, find the comment by commentId, remove the comment from the comments array, save the updated article, and handle cases where the article or comment isn't found, along with other errors.
- Define Mongoose schemas for articles and comments, embedding the comments within the article schema to manage comments as part of the article document.
- Implement POST, GET, and DELETE routes to add, retrieve, and delete comments on articles, linking these routes to their respective handlers (addComment, getCommentsByArticleId, deleteComment).
Frontend:
- Use useEffect to fetch the article and its comments from the backend on component mount, and manage the state for article details, comments, and new comment input using React hooks.
- Implement functions to add and delete comments, updating the comment list in the UI by re-fetching the article or filtering out deleted comments.
- Use Material-UI components to structure the article details, including the title, author, content, categories, tags, and comments, along with input fields and buttons for adding new comments and editing the article.
Backend Code:
JavaScript
// index.js
const express = require('express');
const mongoose = require('mongoose');
const authorRoutes = require('./routes/author');
const articleRoutes = require('./routes/article');
const cors = require('cors');
const connectDB = require('./db/conn');
connectDB();
const app = express();
app.use(express.json());
app.use(cors({
origin: ["http://localhost:5173"],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
credentials: true
}));
mongoose.connect('mongodb://localhost:27017/blog', {
})
.then(() => console.log('MongoDB connected'))
.catch(err => console.log(err));
app.use('/api/articles', articleRoutes);
app.use('/api/authors', authorRoutes);
app.listen(5000, () => {
console.log('Server running on port 5000');
});
JavaScript
// models/article.js
const mongoose = require('mongoose');
const commentSchema = new mongoose.Schema({
content: { type: String, required: true },
author: { type: mongoose.Schema.Types.ObjectId, ref: "Author", required: true },
date: { type: Date, default: Date.now },
});
const articleSchema = new mongoose.Schema({
title: {
type: String,
required: true,
},
content: {
type: String,
required: true,
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Author',
required: true,
},
categories: [{
type: String,
required: false
}],
tags: [{
type: String,
required: false
}],
createdAt: {
type: Date,
default: Date.now,
},
comments: [commentSchema],
});
module.exports = mongoose.model('Article', articleSchema);
JavaScript
// controller/Article.js
const mongoose = require("mongoose");
const Article = require("../models/article");
const Author = require("../models/author");
// Get all articles
const getAllArticles = async (req, res) => {
try {
const articles = await Article.find().populate("author", "name");
res.json(articles);
} catch (err) {
res.status(500).json({ message: err.message });
}
};
// Get a specific article by ID
const getArticleById = async (req, res) => {
try {
const article = await Article.findById(req.params.id).populate(
"author",
"name"
);
if (article) res.json(article);
else res.status(404).json({ message: "Article not found" });
} catch (err) {
res.status(500).json({ message: err.message });
}
};
// Create a new article
const createArticle = async (req, res) => {
const { title, content, author, categories, tags } = req.body;
if (!mongoose.Types.ObjectId.isValid(author)) {
return res.status(400).json({ message: "Invalid author ID" });
}
try {
const existingAuthor = await Author.findById(author);
if (!existingAuthor) {
return res.status(400).json({ message: "Author not found" });
}
const article = new Article({ title, content, author, categories, tags });
const newArticle = await article.save();
res.status(201).json(newArticle);
} catch (err) {
res.status(400).json({ message: err.message });
}
};
// Update an article
const updateArticle = async (req, res) => {
const { title, content, author, categories, tags } = req.body;
try {
const article = await Article.findByIdAndUpdate(
req.params.id,
{ title, content, author, categories, tags },
{ new: true }
).populate("author", "name");
if (article) res.json(article);
else res.status(404).json({ message: "Article not found" });
} catch (err) {
res.status(400).json({ message: err.message });
}
};
// Delete an article
const deleteArticle = async (req, res) => {
try {
const article = await Article.findByIdAndDelete(req.params.id);
if (article) res.json({ message: "Article deleted" });
else res.status(404).json({ message: "Article not found" });
} catch (err) {
res.status(500).json({ message: err.message });
}
};
const getArticlesByFilter = async (req, res) => {
const { category, tag } = req.query;
try {
let query = {};
if (category) query.categories = category;
if (tag) query.tags = tag;
const articles = await Article.find(query).populate("author", "name");
res.json(articles);
} catch (err) {
res.status(500).json({ message: err.message });
}
};
const addComment = async (req, res) => {
const { content, author } = req.body;
const { articleId } = req.params;
if (!mongoose.Types.ObjectId.isValid(author)) {
return res.status(400).json({ message: "Invalid author ID" });
}
try {
const article = await Article.findById(articleId);
if (!article) {
return res.status(404).json({ message: "Article not found" });
}
article.comments.push({ content, author });
await article.save();
res.status(201).json(article);
} catch (err) {
res.status(400).json({ message: err.message });
}
};
// Get all comments for an article
const getCommentsByArticleId = async (req, res) => {
try {
const article = await Article.findById(req.params.articleId)
.populate("comments.author", "name");
if (article) res.json(article.comments);
else res.status(404).json({ message: "Article not found" });
} catch (err) {
res.status(500).json({ message: err.message });
}
};
// Delete a comment
const deleteComment = async (req, res) => {
const { articleId, commentId } = req.params;
try {
const article = await Article.findById(articleId);
if (!article) {
return res.status(404).json({ message: "Article not found" });
}
const commentIndex = article.comments.findIndex(c => c._id.toString() === commentId);
if (commentIndex === -1) {
return res.status(404).json({ message: "Comment not found" });
}
article.comments.splice(commentIndex, 1);
await article.save();
res.json({ message: "Comment deleted" });
} catch (err) {
res.status(500).json({ message: err.message });
}
};
module.exports = {
getAllArticles,
getArticleById,
createArticle,
updateArticle,
deleteArticle,
getArticlesByFilter,
addComment,
getCommentsByArticleId,
deleteComment
};
JavaScript
// routes/article.js
const express = require('express');
const router = express.Router();
const {
getAllArticles,
getArticleById,
createArticle,
updateArticle,
deleteArticle,
getArticlesByFilter,
addComment,
getCommentsByArticleId,
deleteComment
} = require('../controllers/Article');
// Get all articles
router.get('/', getAllArticles);
// Get a specific article by ID
router.get('/:id', getArticleById);
// Create a new article
router.post('/', createArticle);
// Update an article
router.put('/:id', updateArticle);
// Delete an article
router.delete('/:id', deleteArticle);
//filterout with category
router.get('/filter', getArticlesByFilter);
router.post("/:articleId/comments",addComment );
router.get("/:articleId/comments", getCommentsByArticleId);
router.delete("/:articleId/comments/:commentId", deleteComment);
module.exports = router;
Start your server using the below command:
node index.js
Frontend Code:
JavaScript
// App.jsx
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import AuthorList from "./pages/AuthorList";
import CreateAuthor from "./components/CreateAuthor";
import UpdateAuthor from "./components/UpdateAuthor";
import AuthorBio from "./pages/AuthorBio";
import CreateArticle from "./pages/CreateArticle";
import ArticleList from "./pages/ArticleList";
import ArticleDetail from "./pages/ArticleDetail";
const App = () => {
return (
<>
<Router>
<Routes>
<Route path="/" element={<AuthorList />} />
<Route path="/create-author" element={<CreateAuthor />} />
<Route path="/update-author/:id" element={<UpdateAuthor />} />
<Route path="/bio/:id" element={<AuthorBio />} />
<Route path="/create-article" element={<CreateArticle />} />
<Route path="/article" element={<ArticleList />} />
<Route path="/articles/:id" element={<ArticleDetail />} />
</Routes>
</Router>
</>
);
};
export default App;
JavaScript
// src/pages/ArticleDetail.jsx
import DeleteIcon from "@mui/icons-material/Delete";
import {
Avatar,
Box,
Button,
Chip,
Container,
Divider,
IconButton,
List,
ListItem,
ListItemAvatar,
ListItemSecondaryAction,
ListItemText,
TextField,
Typography,
} from "@mui/material";
import axios from "axios";
import React, {useEffect, useState} from "react";
import {
Link,
useLocation,
useParams
} from "react-router-dom";
const ArticleDetail = () => {
const { id } = useParams();
const { search } = useLocation();
const authorId = new URLSearchParams(search).get("authorId");
const [article, setArticle] = useState(null);
const [newComment, setNewComment] = useState("");
const [comments, setComments] = useState([]);
useEffect(() => {
const fetchArticle = async () => {
try {
const response = await axios.get(
`http://localhost:5000/api/articles/${id}`,
{
withCredentials: true,
}
);
console.log("ssss", response.data.author.name);
setArticle(response.data);
setComments(response.data.comments || []);
} catch (error) {
console.error("Error fetching article:", error);
}
};
fetchArticle();
}, [id]);
const handleAddComment = async () => {
try {
await axios.post(
`http://localhost:5000/api/articles/${id}/comments`,
{ content: newComment, author: authorId },
{ withCredentials: true }
);
// Re-fetch the article to get the updated comments with author info
const updatedArticle = await axios.get(
`http://localhost:5000/api/articles/${id}`,
{
withCredentials: true,
}
);
console.log(updatedArticle.data);
setComments(updatedArticle.data.comments || []);
setNewComment("");
} catch (error) {
console.error("Error adding comment:", error);
}
};
const handleDeleteComment = async (commentId) => {
try {
await axios.delete(
`http://localhost:5000/api/articles/${id}/comments/${commentId}`,
{ withCredentials: true }
);
setComments((prevComments) =>
prevComments.filter((comment) => comment._id !== commentId)
);
} catch (error) {
console.error("Error deleting comment:", error);
}
};
if (!article) return <Typography>Loading...</Typography>;
return (
<Container maxWidth="md">
<Typography variant="h3" gutterBottom>
{article.title}
</Typography>
<Typography variant="subtitle1" color="textSecondary" gutterBottom>
By {article.author.name} on{" "}
{new Date(article.createdAt).toLocaleDateString()}
</Typography>
<Divider sx={{ my: 2 }} />
<Typography variant="body1" paragraph>
{article.content}
</Typography>
<Box sx={{ mt: 2 }}>
<Typography variant="h6" gutterBottom>
Categories
</Typography>
<Box sx={{ mb: 2 }}>
{article.categories.map((category, index) => (
<Chip key={index} label={category} sx={{ mr: 1, mb: 1 }} />
))}
</Box>
<Typography variant="h6" gutterBottom>
Tags
</Typography>
<Box>
{article.tags.map((tag, index) => (
<Chip key={index} label={tag} sx={{ mr: 1, mb: 1 }} />
))}
</Box>
</Box>
<Box sx={{ mt: 2 }}>
<Typography variant="h6" gutterBottom>
Comments
</Typography>
<List>
{comments.map((comment) => (
<ListItem key={comment._id}>
<ListItemAvatar>
<Avatar>
{article.author.name[0]}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={comment.content}
secondary={`By ${article.author.name}
on ${new Date(comment.date).toLocaleDateString()}`}
/>
<ListItemSecondaryAction>
<IconButton
edge="end"
aria-label="delete"
onClick={() => handleDeleteComment(comment._id)}
>
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
<Box sx={{ display: "flex", mt: 2 }}>
<TextField
fullWidth
label="Add a comment"
variant="outlined"
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
/>
<Button
variant="contained"
color="primary"
sx={{ ml: 2 }}
onClick={handleAddComment}
>
Post
</Button>
</Box>
</Box>
<Box sx={{ mt: 2 }}>
<Button
variant="contained"
color="primary"
component={Link}
to={`/update-article/${article._id}`}
>
Edit Article
</Button>
</Box>
</Container>
);
};
export default ArticleDetail;
Start your frontend using the below command:
npm start or npm run dev
Output: