Throughout our CMS development series, we've progressively enhanced our application's functionality. After implementing post CRUD operations, we're now focusing on two new features: a "Preview" page that allows authors to review their content before publication, and a status management system to control post visibility. These additions will provide content creators with more control and flexibility in managing their posts. Let's dive into the implementation:
1. Implementing a Dynamic Post Preview Mechanism
We already added a "Preview" button to our "Actions" menu for each post, and now we will simply finish with its functionality.
- add the "onClick" event to the "Edit" button, inside our "TableComponent", with the "handlePreview" function with two parameters;
<MenuItem onClick={(event) => handleEdit(selectedItem, event)}>Edit</MenuItem>
- define the "handlePreview" function, that will take "slug" from the post and navigate the user to the new "preview" route, with dynamic router parameter;
const handlePreview = (item, event) => {
event.preventDefault();
navigate(`/preview/${item.slug}`);
};
- inside the "App.jsx" file we need to import the "Preview" component from the "views" folder, and set this component as an "element" to our new route;
<Route path='preview/:slug' element={<Preview />} />
now, let's create this new "Preview" component inside the "views" folder;
we need to import "useState", "useEffect" hooks from "React", "useParams" from "Router", and "getPostBySlug" function from our services;
create a new function that will fetch posts by "slug" from the database and update our state;
we will need to fetch the post in the "useEffect" hook, before rendering the page, and then return JSX which represents our post page;
const Preview = () => {
const [post, setPost] = useState(null);
const { slug } = useParams();
const [mainImage, setMainImage] = useState(null);
// post: Stores the post data retrieved from the API.
// mainImage: Stores the URL of the main post image.
useEffect(() => {
fetchPost();
}, []);
const fetchPost = async () => {
try {
const response = await getPostBySlug(slug);
if (response.status === 200) {
setPost(response.post);
setMainImage(import.meta.env.VITE_API_BASE_URL + `/posts/image/` + response.post.mainImage.file.name);
}
} catch (error) {
console.log(error);
}
}
// The useEffect hook runs once when the component mounts ([] as the dependency array).
// It calls fetchPost(), which:
// - Retrieves the post using getPostBySlug(slug).
// - If successful (status === 200), it updates post with the response data.
// - Constructs the mainImage URL using import.meta.env.VITE_API_BASE_URL.
return (
<div className="preview-page">
<section className="preview-page__content">
<h1 className="preview-page__content--title">{post?.title}</h1>
<p className='preview-page__content--subtitle'>{post?.subTitle}</p>
// Displays the post title and subtitle.
<div className='preview-page__content--image'>
<img className='preview-page__content--image-pic' src={mainImage} alt={post?.mainImage?.alt} />
</div>
// Renders the mainImage.
<div>
{post?.body.map((item, idx) => {
switch (item.type) {
case 'text':
return (
<div key={idx} dangerouslySetInnerHTML={{ __html: item.content }} className='preview-page__content--text'/>
);
case 'image':
return (
<div className='preview-page__content--image' key={idx}>
<img
src={import.meta.env.VITE_API_BASE_URL + `/posts/image/` + item.file.name}
alt={item.alt || 'Image'}
className='preview-page__content--image-pic'/>
</div>
);
default:
return null;
}
})
}
// Iterates over post.body to render different types of content dynamically:
// - Text (type: "text"): Uses dangerouslySetInnerHTML to render HTML content.
// - Image (type: "image"): Displays images dynamically with their source and alt text.
</div>
</section>
</div>
);
};
Cool, cool, cool... Let's relaunch the app and check the result, or let's check it at the end of this article.
2. Managing Post Visibility: Status Control System
We will need to define which post to show to the clients, and which is not, and for that case, we added a "status" value, and the "activate - deactivate" button to our "Action" menu.
To change the article "status" we will create an additional endpoint that will have a "partial update" functionality so that if we need to update another field we could use this endpoint also. In this case, we will start from the frontend first:
- create a new "partial update" function inside the "posts.services.js" file, that will send a "Post" request and data to our server;
export const postPartialUpdate = (data) =>
HTTP.post('/posts/partial-update', data).then(({data}) => data );
- update the "Action" menu in the "Table.component" with the "status update" logic;
<MenuItem onClick={(event) => handleStatus(selectedItem, event)}>{
selectedItem?.status === 'online'
? 'Deactivate'
: 'Activate'
}</MenuItem>
- next, we need to define a "handleStatus" function, then create a new object that will store an article id and new status, then send this object to our endpoint and get an updated posts list;
const handleStatus = async (item, event) => {
event.preventDefault();
try {
const newUpdate = {
_id: item._id,
status: item.status === "offline" ? "online" : "offline"
}
await postPartialUpdate({post: newUpdate});
const data = await getPostsList();
dispatch(aSetPostsList(data.posts));
dispatch(aPushNewNotification({ type: 'success', text: 'Post status updated' }));
} catch (error) {
console.error("Error updating Post status:", error);
dispatch(aPushNewNotification({ type: 'error', text: 'Failed to update Post status' }));
}
handleClose();
};
Nice, now we need to configure our backend so that our server will receive data and update the article we need to be updated.
- add a new route inside the "posts.router.js" file;
postsRouter.post('/partial-update', protect, postsController.partialUpdatePost);
- create a new controller that will get data from the request and send it to our "posts" model;
async function partialUpdatePost(req, res) {
const { post } = req.body;
try {
const updatedPost = await postsModel.partialUpdatePost(post);
return res.status(200).json({
status: 200,
message: 'Post updated successfully',
post: updatedPost
});
} catch (error) {
return res.status(500).json({
status: 500,
message: 'Internal server error'
});
}
}
- add a new model function that gets payload and then finds an "updateOne" article status or archive value from the post;
async function partialUpdatePost(payload) {
try {
if (payload.status) {
const newStatus = payload.status;
const updatedPost = await posts.updateOne({ _id: payload._id }, {$set: { status: newStatus }});
return updatedPost;
}
if (payload.archived) {
const newArchived = payload.archived;
const updatedPost = await posts.updateOne({ _id: payload._id }, {$set: { archived: newArchived }});
return updatedPost;
}
} catch (error) {
console.error('Error updating post:', error);
throw error;
}
}
In the future, we can update this model and make it dynamical, so that it would search and replace fields without the necessity to add instructions manually.
Okay, let's relaunch our app and check the result.
Throughout this development phase, we've enhanced our CMS with two essential features that work together to create a more powerful content management experience. The new preview system ensures content creators can confidently review their work before publication, eliminating uncertainty about how posts will appear to readers. This combines with our new status control system, which provides precise management over post visibility through a partial update mechanism.
The architecture we've implemented is particularly noteworthy for its extensibility. While currently focused on status updates and content preview, the partial update system we've built serves as a foundation for future enhancements. This approach ensures that as our CMS grows and evolves, we can easily integrate new features without significant structural changes to our codebase.
The complete code for this tutorial is available in the repository. If you have any questions or run into issues, feel free to leave them in the comments section below.
Found this post useful? ☕ A coffee-sized contribution goes a long way in keeping me inspired! Thank you)
Next step: "Smart Content Management: Integrating Search, Filters, and Pagination with React and Node.js"
Top comments (0)