Display Related Articles in MERN Blogs and News Website

Last Updated : 16 May, 2025

When you're diving into an interesting blog post or news article, it's always exciting to discover more content that captures your interest. At our blogs and news website, we're all about making your reading experience as enriching and enjoyable as possible. That's why we offer a feature that highlights related articles, helping you find more of what you love.

  • Ever finished reading an article and wished you could find more on the same topic? Related articles are here to help with that! They offer several benefits:
  • Enhance Your Knowledge: Find articles that expand on the topic you've just read about. If you enjoyed a piece on sustainable living, related articles might include tips on eco-friendly practices or profiles of green innovators.
  • Stay Updated: Get the latest updates on ongoing stories or trends. If you’re reading about a current event, related articles can provide additional context or updates on the situation.
  • Discover New Interests: You might find a new passion or interest through related articles. Reading about one topic might lead you to explore others you hadn't considered before.

How It Works

  • Our related articles feature is designed to be intuitive and helpful
  • Contextual Matching: As you read an article, our system identifies key topics and themes. It then suggests other articles that cover similar or related subjects.
  • Curated Recommendations: We use a mix of algorithms and editorial expertise to recommend articles that we think you'll find interesting and relevant.
  • Seamless Integration: Related articles are displayed in a sidebar or at the end of the article, making it easy to browse without interrupting your reading flow.

Backend:

  • Fetch the current article to retrieve its categories or tags, then query for other articles with matching categories or tags, excluding the current article, and limit the results.
  • Handle errors by sending appropriate status codes and messages.

Frontend:

  • Retrieve related articles based on the current article’s categories or tags by making a GET request to the API.
  • Store the fetched related articles in the relatedArticles state variable.
  • Display related articles using a List component with links to each article, showing titles and publication dates.

Backend Code:

JavaScript
// index.js

const express = require("express");
const mongoose = require("mongoose");
const authorRoutes = require("./routes/author");
const articleRoutes = require("./routes/article");
const newsletterRoutes = require("./routes/newsletter");
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.use("/api/newsletter", newsletterRoutes);
app.listen(
    5000,
    () => { console.log("Server running on port 5000"); });
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 getRelatedArticles = async (req, res) => {
    const { id } = req.params;

    try {
        // Fetch the current article to get its categories or tags
        const currentArticle = await Article.findById(id);
        if (!currentArticle) {
            return res.status(404).json({ message: 'Article not found' });
        }

        // Find articles with matching categories or tags
        const relatedArticles = await Article.find({
            _id: { $ne: id },
            $or: [
                { categories: { $in: currentArticle.categories } },
                { tags: { $in: currentArticle.tags } }
            ]
        }).populate("author", "name").limit(5); // Limit the number of related articles

        res.json(relatedArticles);
    } 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,
    getRelatedArticles
    
};
JavaScript
// routes/article.js

const express = require('express');
const router = express.Router();
const {
    getAllArticles,
    getArticleById,
    createArticle,
    updateArticle,
    deleteArticle,
    getArticlesByFilter,
    addComment,
    getCommentsByArticleId,
    deleteComment,
    getRelatedArticles
} = 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);

router.get('/:id/related', getRelatedArticles);

module.exports = router;
JavaScript
/ models/article.js

const express = require('express');
const router = express.Router();
const {
    getAllArticles,
    getArticleById,
    createArticle,
    updateArticle,
    deleteArticle,
    getArticlesByFilter,
    addComment,
    getCommentsByArticleId,
    deleteComment,
    getRelatedArticles
} = 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);

router.get('/:id/related', getRelatedArticles);

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 React, { useState, useEffect } from "react";
import axios from "axios";
import { useParams, Link, useLocation } from "react-router-dom";
import {
    Container,
    Typography,
    Box,
    Button,
    Divider,
    Chip,
    TextField,
    IconButton,
    List,
    ListItem,
    ListItemText,
    ListItemSecondaryAction,
    ListItemAvatar,
    Avatar,
} from "@mui/material";
import DeleteIcon from '@mui/icons-material/Delete';
import NewsletterSubscription from "../components/NewsletterSubscription";

const ArticleDetail = () => {
    const { id } = useParams();
    const { search } = useLocation();
    const authorId = new URLSearchParams(search).get("authorId");  // Get authorId from URL query
    const [article, setArticle] = useState(null);
    const [newComment, setNewComment] = useState("");
    const [comments, setComments] = useState([]);
    const [relatedArticles, setRelatedArticles] = useState([]);

    useEffect(() => {
        const fetchArticle = async () => {
            try {
                const response = await axios.get(
                    `http://localhost:5000/api/articles/${id}`,
                    {
                        withCredentials: true,
                    }
                );
                setArticle(response.data);
                setComments(response.data.comments || []);
                
                // Fetch related articles
                const relatedResponse = await axios.get(
                    `http://localhost:5000/api/articles/${id}/related`,
                    {
                        withCredentials: true,
                    }
                );
                setRelatedArticles(relatedResponse.data);
            } 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,
                }
            );
            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>
            <Box sx={{ mt: 4 }}>
                <Typography variant="h5" gutterBottom>
                    Related Articles
                </Typography>
                <List>
                    {relatedArticles.map((relatedArticle) => (
                        <ListItem key={relatedArticle._id}>
                            <ListItemAvatar>
                                <Avatar>
                                    {relatedArticle.author.name[0]} 
                                </Avatar>
                            </ListItemAvatar>
                            <ListItemText
                                primary={
                                    <Link to={`/articles/${relatedArticle._id}`}>
                                        {relatedArticle.title}
                                    </Link>
                                }
                                secondary={`By ${relatedArticle.author.name} 
                                on ${new Date(relatedArticle.createdAt).toLocaleDateString()}`}
                            />
                        </ListItem>
                    ))}
                </List>
            </Box>
            <NewsletterSubscription />
        </Container>
    );
};

export default ArticleDetail;

Start tour frontend using the below command:

npm start or npm run dev

Output

Conclusion

In summary, the related articles feature is a simple yet powerful way to enhance your reading experience. By automatically suggesting content based on what you're currently reading, it helps you learn more, stay informed, and explore new interests with ease. Whether you're catching up on news or diving into blog topics, related articles make it easy to find more of what you love—all without disrupting your flow.

Comment

Explore