Build a Next.js Interactive Web-Based Social Media Feed

Written by

in

In today’s digital age, social media has become an integral part of our lives, connecting us with friends, family, and the world at large. Imagine building your own social media feed, a personalized space where you can curate content from various sources, engage with posts, and share your thoughts. This tutorial will guide you through creating a dynamic and interactive social media feed using Next.js, a powerful React framework that simplifies web development. We’ll cover everything from setting up your project to implementing features like fetching data, displaying posts, and handling user interactions. This project is perfect for beginners to intermediate developers looking to expand their skills and create engaging web applications.

Why Build a Social Media Feed?

Building a social media feed is an excellent way to learn and practice various web development concepts. It allows you to:

  • Master Data Fetching: Learn how to retrieve data from APIs and display it dynamically.
  • Understand State Management: Handle user interactions and update the UI accordingly.
  • Explore UI Design: Create a visually appealing and user-friendly interface.
  • Enhance Your Portfolio: Showcase your skills by creating a practical and engaging project.

This tutorial will provide you with a solid foundation in Next.js and frontend development, equipping you with the skills to build more complex applications in the future.

Project Setup: Getting Started with Next.js

Before diving into the code, let’s set up our development environment. Make sure you have Node.js and npm (or yarn) installed. Then, create a new Next.js project using the following command in your terminal:

npx create-next-app social-media-feed

This command will create a new directory called social-media-feed with all the necessary files and dependencies. Navigate into the project directory:

cd social-media-feed

Now, start the development server:

npm run dev

Open your browser and go to http://localhost:3000. You should see the default Next.js welcome page. This confirms that your project is set up correctly.

Fetching Data: Simulating a Social Media API

In a real-world scenario, you would fetch data from a social media API (e.g., Twitter, Instagram, or a custom backend). For this tutorial, we’ll simulate an API by creating a simple JSON file that holds our post data. Create a new directory called data in your project’s root directory. Inside the data directory, create a file named posts.json. Add the following sample data:

[
  {
    "id": 1,
    "username": "user1",
    "profile_picture": "/images/profile1.jpg",
    "content": "Hello, world! This is my first post.",
    "timestamp": "2024-01-20T10:00:00Z",
    "likes": 10,
    "comments": [
      {"username": "user2", "comment": "Great post!"}
    ]
  },
  {
    "id": 2,
    "username": "user2",
    "profile_picture": "/images/profile2.jpg",
    "content": "Enjoying a beautiful day.",
    "timestamp": "2024-01-20T12:00:00Z",
    "likes": 25,
    "comments": [
      {"username": "user1", "comment": "Looks amazing!"},
      {"username": "user3", "comment": "Where are you?"}
    ]
  }
]

This JSON file represents an array of posts, each with properties like id, username, profile_picture, content, timestamp, likes, and comments. Now, let’s create a function to fetch this data in our Next.js application.

Displaying Posts: Building the UI

Open the pages/index.js file. This is the main page of your application. Replace the existing code with the following:

import { useState, useEffect } from 'react';
import styles from '../styles/Home.module.css';

function HomePage() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchPosts() {
      try {
        const response = await fetch('/api/posts'); // Assuming you'll create an API route
        const data = await response.json();
        setPosts(data);
      } catch (error) {
        console.error('Error fetching posts:', error);
      } finally {
        setLoading(false);
      }
    }

    fetchPosts();
  }, []);

  if (loading) {
    return <p>Loading posts...</p>;
  }

  return (
    <div>
      <h1>Social Media Feed</h1>
      {posts.map((post) => (
        <div>
          <div>
            <img src="{post.profile_picture}" alt="{post.username}" />
            <div>
              <p>{post.username}</p>
              <p>{new Date(post.timestamp).toLocaleString()}</p>
            </div>
          </div>
          <p>{post.content}</p>
          <div>
            <button>❤️ {post.likes}</button>
            {/* Add comment button and other actions here */}
          </div>
          <div>
            {post.comments.map((comment, index) => (
              <div>
                <p>{comment.username}:</p>
                <p>{comment.comment}</p>
              </div>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}

export default HomePage;

Let’s break down the code:

  • Import Statements: Imports necessary modules from React and a stylesheet.
  • State Variables: posts stores the fetched posts, and loading indicates whether data is being fetched.
  • useEffect Hook: Fetches posts from the API route (which we will create soon) when the component mounts.
  • Loading State: Displays “Loading posts…” while data is being fetched.
  • Mapping Posts: Iterates through the posts array and renders each post using the map function.
  • Post Structure: Each post is displayed with its username, profile picture, content, timestamp, likes, and comments.

Now, create the API route to serve the data from posts.json. In the pages/api directory, create a file named posts.js. Add the following code:

import fs from 'fs';
import path from 'path';

export default function handler(req, res) {
  const filePath = path.join(process.cwd(), 'data', 'posts.json');
  const jsonData = fs.readFileSync(filePath, 'utf8');
  const data = JSON.parse(jsonData);

  res.status(200).json(data);
}

This API route reads the posts.json file and returns its content as a JSON response. It uses the Node.js fs and path modules to read the file from the file system. The process.cwd() function returns the current working directory, which is the root of your Next.js project.

Important: You’ll need to create an /images directory in the public folder and add profile pictures with the names profile1.jpg and profile2.jpg.

Finally, create a CSS module for styling. Create a file named styles/Home.module.css and add some basic styles:

.container {
  width: 100%;
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
  font-family: sans-serif;
}

.post {
  border: 1px solid #ccc;
  margin-bottom: 20px;
  padding: 10px;
  border-radius: 5px;
}

.postHeader {
  display: flex;
  align-items: center;
  margin-bottom: 10px;
}

.profilePicture {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  margin-right: 10px;
}

.userInfo {
  display: flex;
  flex-direction: column;
}

.username {
  font-weight: bold;
}

.timestamp {
  font-size: 0.8em;
  color: #777;
}

.postContent {
  margin-bottom: 10px;
}

.postActions {
  display: flex;
  align-items: center;
}

.likeButton {
  background-color: #f0f0f0;
  border: none;
  padding: 5px 10px;
  border-radius: 5px;
  cursor: pointer;
}

.commentsSection {
  margin-top: 10px;
  padding-left: 10px;
  border-left: 2px solid #eee;
}

.comment {
  margin-bottom: 5px;
}

.commentUsername {
  font-weight: bold;
  margin-right: 5px;
}

.commentText {
  color: #333;
}

Refresh your browser. You should now see the posts displayed on the page, with the username, profile picture, content, timestamp, likes, and comments. Congratulations! You’ve successfully fetched data, displayed it, and built a basic UI for your social media feed.

Adding User Interactions: Likes and Comments

Let’s add some interactivity to our feed. We’ll implement a like button and a basic comment section.

Implementing the Like Button

First, modify the HomePage component in pages/index.js to include the like button functionality:


import { useState, useEffect } from 'react';
import styles from '../styles/Home.module.css';

function HomePage() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchPosts() {
      try {
        const response = await fetch('/api/posts');
        const data = await response.json();
        setPosts(data);
      } catch (error) {
        console.error('Error fetching posts:', error);
      } finally {
        setLoading(false);
      }
    }

    fetchPosts();
  }, []);

  const handleLike = async (postId) => {
    // Optimistically update the UI
    setPosts(prevPosts =>
      prevPosts.map(post =>
        post.id === postId ? { ...post, likes: post.likes + 1 } : post
      )
    );

    try {
      const response = await fetch(`/api/like?postId=${postId}`, {
        method: 'POST',
      });

      if (!response.ok) {
        throw new Error('Failed to update like');
      }

    } catch (error) {
      console.error('Error liking post:', error);
      // Revert the optimistic update on error
      setPosts(prevPosts =>
        prevPosts.map(post =>
          post.id === postId ? { ...post, likes: post.likes - 1 } : post
        )
      );
    }
  };

  if (loading) {
    return <p>Loading posts...</p>;
  }

  return (
    <div>
      <h1>Social Media Feed</h1>
      {posts.map((post) => (
        <div>
          <div>
            <img src="{post.profile_picture}" alt="{post.username}" />
            <div>
              <p>{post.username}</p>
              <p>{new Date(post.timestamp).toLocaleString()}</p>
            </div>
          </div>
          <p>{post.content}</p>
          <div>
            <button> handleLike(post.id)}>❤️ {post.likes}</button>
            {/* Add comment button and other actions here */}
          </div>
          <div>
            {post.comments.map((comment, index) => (
              <div>
                <p>{comment.username}:</p>
                <p>{comment.comment}</p>
              </div>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}

export default HomePage;

Here’s what changed:

  • handleLike Function: This function is triggered when the like button is clicked.
  • Optimistic Update: The likes count is immediately incremented in the UI using setPosts. This provides instant feedback to the user.
  • API Call: A POST request is made to the /api/like endpoint (which we’ll create next) to update the like count on the server.
  • Error Handling: If the API call fails, the UI is reverted to its previous state to ensure data consistency.
  • onClick Event: The onClick event is added to the like button, calling the handleLike function with the post ID.

Now, create the API route for handling likes. In the pages/api directory, create a file named like.js. Add the following code:

import fs from 'fs';
import path from 'path';

export default function handler(req, res) {
  if (req.method === 'POST') {
    const postId = parseInt(req.query.postId, 10); // Get the post ID from the query parameters
    const filePath = path.join(process.cwd(), 'data', 'posts.json');

    try {
      const jsonData = fs.readFileSync(filePath, 'utf8');
      const posts = JSON.parse(jsonData);

      // Find the post and increment the likes
      const updatedPosts = posts.map(post => {
        if (post.id === postId) {
          return { ...post, likes: post.likes + 1 };
        }
        return post;
      });

      // Write the updated data back to the file
      fs.writeFileSync(filePath, JSON.stringify(updatedPosts, null, 2), 'utf8');

      res.status(200).json({ message: 'Like updated' });
    } catch (error) {
      console.error('Error updating like:', error);
      res.status(500).json({ error: 'Failed to update like' });
    }
  } else {
    res.status(405).json({ error: 'Method Not Allowed' });
  }
}

This API route handles POST requests to update the like count for a specific post. It reads the posts.json file, finds the post with the matching ID, increments the likes, and writes the updated data back to the file. It also includes error handling.

Important: This is a simplified example. In a real application, you would typically use a database to store and manage your data. Also, add the following line to the top of the file pages/api/posts.js: import { promises as fs } from 'fs'; and change the lines that read and write files to use the asynchronous methods, to avoid blocking the event loop.

  const jsonData = await fs.readFile(filePath, 'utf8');
  await fs.writeFile(filePath, JSON.stringify(updatedPosts, null, 2), 'utf8');

Implementing the Comment Section

Next, let’s add a comment section to each post. Modify the HomePage component in pages/index.js to include a comment input and a button to submit comments:


import { useState, useEffect } from 'react';
import styles from '../styles/Home.module.css';

function HomePage() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [commentText, setCommentText] = useState('');
  const [activePostId, setActivePostId] = useState(null);

  useEffect(() => {
    async function fetchPosts() {
      try {
        const response = await fetch('/api/posts');
        const data = await response.json();
        setPosts(data);
      } catch (error) {
        console.error('Error fetching posts:', error);
      } finally {
        setLoading(false);
      }
    }

    fetchPosts();
  }, []);

  const handleLike = async (postId) => {
    // Optimistically update the UI
    setPosts(prevPosts =>
      prevPosts.map(post =>
        post.id === postId ? { ...post, likes: post.likes + 1 } : post
      )
    );

    try {
      const response = await fetch(`/api/like?postId=${postId}`, {
        method: 'POST',
      });

      if (!response.ok) {
        throw new Error('Failed to update like');
      }

    } catch (error) {
      console.error('Error liking post:', error);
      // Revert the optimistic update on error
      setPosts(prevPosts =>
        prevPosts.map(post =>
          post.id === postId ? { ...post, likes: post.likes - 1 } : post
        )
      );
    }
  };

  const handleCommentChange = (event) => {
    setCommentText(event.target.value);
  };

  const handleCommentSubmit = async (postId) => {
    if (!commentText.trim()) return; // Prevent empty comments

    // Optimistically add the comment to the UI
    setPosts(prevPosts =>
      prevPosts.map(post => {
        if (post.id === postId) {
          return {
            ...post,
            comments: [...post.comments, { username: 'You', comment: commentText }],
          };
        }
        return post;
      })
    );

    setCommentText(''); // Clear the comment input
    setActivePostId(null);  // Optionally close comment input

    try {
      const response = await fetch('/api/comment', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ postId, comment: commentText }),
      });

      if (!response.ok) {
        throw new Error('Failed to add comment');
      }

    } catch (error) {
      console.error('Error adding comment:', error);
      // Revert the optimistic update on error
      setPosts(prevPosts =>
        prevPosts.map(post => {
          if (post.id === postId) {
            return {
              ...post,
              comments: post.comments.slice(0, -1), // Remove the last comment (optimistically added)
            };
          }
          return post;
        })
      );
    }
  };

  if (loading) {
    return <p>Loading posts...</p>;
  }

  return (
    <div>
      <h1>Social Media Feed</h1>
      {posts.map((post) => (
        <div>
          <div>
            <img src="{post.profile_picture}" alt="{post.username}" />
            <div>
              <p>{post.username}</p>
              <p>{new Date(post.timestamp).toLocaleString()}</p>
            </div>
          </div>
          <p>{post.content}</p>
          <div>
            <button> handleLike(post.id)}>❤️ {post.likes}</button>
            {/* Comment Section */}
          </div>
          <div>
            {post.comments.map((comment, index) => (
              <div>
                <p>{comment.username}:</p>
                <p>{comment.comment}</p>
              </div>
            ))} 
            <div>
              
              <button> handleCommentSubmit(post.id)} className={styles.commentButton}>Comment</button>
            </div>
          </div>
        </div>
      ))}
    </div>
  );
}

export default HomePage;

Key changes include:

  • commentText State: Manages the text input for comments.
  • activePostId State: Manages if the comment input is open.
  • handleCommentChange Function: Updates the commentText state when the user types in the comment input.
  • handleCommentSubmit Function: Handles the submission of comments. It adds the comment to the UI optimistically, makes an API call to save the comment, and reverts the UI if the API call fails.
  • Comment Input: Added an input field and a button for submitting comments.

Next, create the API route for handling comments. In the pages/api directory, create a file named comment.js. Add the following code:

import fs from 'fs';
import path from 'path';

export default function handler(req, res) {
  if (req.method === 'POST') {
    const { postId, comment } = JSON.parse(req.body);
    const filePath = path.join(process.cwd(), 'data', 'posts.json');

    try {
      const jsonData = fs.readFileSync(filePath, 'utf8');
      const posts = JSON.parse(jsonData);

      const updatedPosts = posts.map(post => {
        if (post.id === parseInt(postId, 10)) {
          return {
            ...post,
            comments: [...post.comments, { username: 'You', comment }],
          };
        }
        return post;
      });

      fs.writeFileSync(filePath, JSON.stringify(updatedPosts, null, 2), 'utf8');

      res.status(200).json({ message: 'Comment added' });
    } catch (error) {
      console.error('Error adding comment:', error);
      res.status(500).json({ error: 'Failed to add comment' });
    }
  } else {
    res.status(405).json({ error: 'Method Not Allowed' });
  }
}

This API route handles POST requests to add a comment to a specific post. It reads the posts.json file, finds the post with the matching ID, adds the new comment, and writes the updated data back to the file. It also includes error handling.

Important: As before, consider using asynchronous methods for file operations and replacing the data storage to a proper database in a real-world application.

Now, refresh your browser. You should now be able to like posts and add comments. This completes the core functionality of your social media feed.

Common Mistakes and Troubleshooting

Here are some common mistakes and how to fix them:

  • Incorrect File Paths: Double-check that your file paths (e.g., for the posts.json file and profile pictures) are correct.
  • CORS Errors: If you encounter CORS (Cross-Origin Resource Sharing) errors, make sure your API routes are correctly configured to handle requests from your frontend. You might need to install and configure a CORS middleware.
  • Data Not Updating: If your data isn’t updating, make sure your API routes are correctly writing the updated data back to the posts.json file (or your database). Also, ensure that your frontend is correctly fetching the updated data after the changes.
  • Typographical Errors: Always check for typos in your code. These can cause unexpected behavior.
  • Network Errors: Use your browser’s developer tools (Network tab) to check for any network requests that are failing. This can help you identify issues with your API routes or data fetching.

SEO Best Practices

To ensure your social media feed ranks well in search results, follow these SEO best practices:

  • Use Relevant Keywords: Incorporate relevant keywords (e.g., “social media feed”, “Next.js”, “React”) naturally throughout your content, including your title, headings, and body text.
  • Optimize Meta Descriptions: Write a concise and compelling meta description (max 160 characters) that accurately describes your page content.
  • Use Descriptive Titles: Create a clear and descriptive title for your page that includes your target keywords. Keep the title length under 70 characters.
  • Use Heading Tags: Use heading tags (<h1> to <h6>) to structure your content and make it easier for search engines to understand.
  • Optimize Images: Compress your images to reduce file size and improve page loading speed. Use descriptive alt text for your images.
  • Ensure Mobile-Friendliness: Make sure your website is responsive and looks good on all devices.
  • Build High-Quality Content: Provide valuable and original content that users will find helpful and engaging.
  • Get Backlinks: Encourage other websites to link to your content.

Key Takeaways

By following this tutorial, you’ve learned how to build a dynamic and interactive social media feed using Next.js. You’ve gained experience with data fetching, UI design, state management, and user interactions. You can now use these skills to build more complex and engaging web applications. Remember to always test your code thoroughly and to learn from any mistakes you encounter.

FAQ

Q: How can I deploy this application?

A: You can deploy your Next.js application to platforms like Vercel, Netlify, or AWS. Vercel is the easiest option as it’s specifically designed for Next.js applications.

Q: How can I add pagination to my feed?

A: You can implement pagination by fetching a limited number of posts initially and then fetching more posts as the user scrolls down or clicks a “Load More” button. You’ll need to modify your API route to support pagination.

Q: How can I add user authentication?

A: You can use libraries like NextAuth.js or Firebase Authentication to add user authentication to your application. This will allow users to create accounts, log in, and interact with the feed in a personalized way.

Q: How can I improve the performance of my feed?

A: You can improve the performance by optimizing images, using code splitting, and implementing server-side rendering or static site generation where appropriate. Consider using a CDN to serve your static assets.

By taking this project further, you’ll not only enhance your skills but also create a valuable tool, a personalized space where you can share your thoughts, and connect with others. The possibilities are vast, and the journey of building this social media feed is an exciting step in your web development adventure.