Adding a newsletter subscription feature to your blogs and news websites can enhance user engagement and keep your audience updated with the latest content. This article will guide you through implementing a newsletter subscription system, covering both backend and frontend aspects.
Why Offer a Newsletter Subscription?
A newsletter subscription feature allows you to build a direct line of communication with your readers. Here are a few key benefits:
- Direct Engagement: Reach your audience directly in their inbox with the latest updates, articles, and exclusive content.
- Increased Traffic: Regular newsletters can drive more traffic to your website, as subscribers are likely to revisit new content.
- Personalization: Design content to subscriber preferences and behaviours, enhancing the user experience.
Approach to Implement Newsletter Subscription
Backend:
- Create a Mongoose model (NewsletterSubscriber) to store subscriber emails with validation.
- Implement a controller function (subscribeToNewsletter) to handle subscription logic, checking for duplicates, and saving new subscribers.
- Define a route (/subscribe) in your Express app to handle POST requests for subscriptions.
- Connect the route to the controller in your Express router to process subscription requests and respond accordingly.
Frontend:
- Create State Variables: Manage email, open, message, and severity using React's useState.
- Handle Subscription: Send a POST request with axios to your API endpoint on button click, and update the UI based on the response.
- Display Feedback: Use Snackbar and Alert from MUI to show success or error messages.
- Render Component: Provide an email input field, a subscribe button, and feedback for user interaction.
Backend Code:
// 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"); });
// models/NewsletterSubscriber.js
const mongoose = require("mongoose");
const newsletterSchema = new mongoose.Schema({
email : {
type : String,
required : true,
unique : true,
lowercase : true,
trim : true
},
subscribedAt : {type : Date, default : Date.now}
});
const NewsletterSubscriber = mongoose.model(
"NewsletterSubscriber", newsletterSchema);
module.exports = NewsletterSubscriber;
// controllers/newsletterController.js
const NewsletterSubscriber
= require("../models/NewsletterSubscriber");
// Controller to handle subscribing to the newsletter
const subscribeToNewsletter = async (req, res) => {
const {email} = req.body;
if (!email) {
return res.status(400).json(
{message : "Email is required"});
}
try {
// Check if the email is already subscribed
const existingSubscriber
= await NewsletterSubscriber.findOne({email});
if (existingSubscriber) {
return res.status(400).json(
{message : "Email is already subscribed"});
}
// Create a new subscriber
const newSubscriber
= new NewsletterSubscriber({email});
await newSubscriber.save();
res.status(200).json(
{message : "Subscription successful"});
}
catch (error) {
console.error("Error subscribing to newsletter:",
error);
res.status(500).json(
{message : "Internal Server Error"});
}
};
module.exports = {subscribeToNewsletter};
// routes/newsletter.js
const express = require("express");
const router = express.Router();
const {subscribeToNewsletter} = require("../controllers/NewsletterSubscriber");
// Route to subscribe to the newsletter
router.post("/subscribe", subscribeToNewsletter);
module.exports = router;
Start your server using the below command:
node index.jsFrontend Code:
// src/components/NewsletterSubscription.js
import React, { useState } from 'react';
import { Button, TextField, Typography, Box, Snackbar } from '@mui/material';
import { Alert } from '@mui/material';
import axios from 'axios';
const NewsletterSubscription = () => {
const [email, setEmail] = useState('');
const [open, setOpen] = useState(false);
const [message, setMessage] = useState('');
const [severity, setSeverity] = useState('success');
const handleSubscribe = async () => {
try {
const response = await axios.post
('http://localhost:5000/api/newsletter/subscribe', { email });
setMessage(response.data.message);
setSeverity('success');
} catch (error) {
setMessage(error.response?.data?.message || 'Something went wrong');
setSeverity('error');
}
setOpen(true);
};
return (
<Box sx={{ mt: 4 }}>
<Typography variant="h5" gutterBottom>
Subscribe to Our Newsletter
</Typography>
<TextField
fullWidth
label="Email Address"
variant="outlined"
value={email}
onChange={(e) => setEmail(e.target.value)}
sx={{ mb: 2 }}
/>
<Button variant="contained" color="primary" onClick={handleSubscribe}>
Subscribe
</Button>
<Snackbar
open={open}
autoHideDuration={6000}
onClose={() => setOpen(false)}
>
<Alert onClose={() => setOpen(false)} severity={severity}>
{message}
</Alert>
</Snackbar>
</Box>
);
};
export default NewsletterSubscription;
// 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");
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>
<NewsletterSubscription/>
</Container>
);
};
export default ArticleDetail;
Start your frontend using the below command:
npm start or npm run dev