DEV Community

Cover image for CMS Development: Adding Post Preview and Status Management in React and Node.js
WebCraft Notes
WebCraft Notes

Posted on • Originally published at webcraft-notes.com

CMS Development: Adding Post Preview and Status Management in React and Node.js

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>
Enter fullscreen mode Exit fullscreen mode
  • 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}`);
};
Enter fullscreen mode Exit fullscreen mode
  • 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 />} />
Enter fullscreen mode Exit fullscreen mode
  • 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>
    );
};
Enter fullscreen mode Exit fullscreen mode

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 );
Enter fullscreen mode Exit fullscreen mode
  • 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>
Enter fullscreen mode Exit fullscreen mode
  • 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();
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
  • 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'
        });
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

post preview system

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)
Buy Me A Coffee

Next step: "Smart Content Management: Integrating Search, Filters, and Pagination with React and Node.js"

Top comments (0)