In today’s e-commerce landscape, users expect a seamless and efficient shopping experience. A crucial part of this experience is the ability to easily browse and filter products. Imagine a user landing on a website with hundreds or even thousands of items. Without effective filtering, finding the right product can feel like searching for a needle in a haystack. This tutorial will guide you through building a dynamic product catalog with filtering capabilities using Next.js, a powerful React framework for building modern web applications. We’ll focus on creating a user-friendly interface that allows users to quickly narrow down their choices, improving engagement and, ultimately, sales.
Why Next.js?
Next.js offers several advantages for this project:
- Server-Side Rendering (SSR) and Static Site Generation (SSG): Next.js allows us to pre-render our product catalog, improving SEO and initial page load times. This is crucial for user experience and search engine rankings.
- Routing: Next.js simplifies routing, making it easy to create different pages for product categories and individual product details.
- API Routes: Next.js provides a straightforward way to create API endpoints for fetching product data and handling user interactions.
- Built-in Optimization: Next.js automatically optimizes images and other assets, ensuring a fast and performant website.
Project Setup
Let’s get started by setting up our Next.js project. Open your terminal and run the following command:
npx create-next-app product-catalog-app
cd product-catalog-app
This will create a new Next.js project named ‘product-catalog-app’. Now, install some dependencies we’ll need:
npm install react-icons
We’ll use ‘react-icons’ for adding icons to our UI.
Data Structure
Before we build the UI, let’s define how our product data will look. For simplicity, we’ll use a local JSON file to store our product information. Create a file named `products.json` in the root directory of your project and add some sample product data:
[
{
"id": 1,
"name": "Awesome T-Shirt",
"description": "A comfortable and stylish t-shirt.",
"image": "/images/tshirt.jpg",
"price": 25,
"category": "clothing",
"color": "blue",
"size": "M",
"available": true
},
{
"id": 2,
"name": "Durable Jeans",
"description": "High-quality jeans for everyday wear.",
"image": "/images/jeans.jpg",
"price": 75,
"category": "clothing",
"color": "blue",
"size": "32",
"available": true
},
{
"id": 3,
"name": "Running Shoes",
"description": "Comfortable and supportive running shoes.",
"image": "/images/shoes.jpg",
"price": 90,
"category": "shoes",
"color": "black",
"size": "10",
"available": true
},
{
"id": 4,
"name": "Laptop Backpack",
"description": "A stylish backpack with laptop compartment.",
"image": "/images/backpack.jpg",
"price": 50,
"category": "bags",
"color": "gray",
"size": "",
"available": true
},
{
"id": 5,
"name": "Wireless Headphones",
"description": "Over-ear headphones with great sound quality.",
"image": "/images/headphones.jpg",
"price": 150,
"category": "electronics",
"color": "black",
"size": "",
"available": true
}
]
You can add more products to this file as needed. Make sure you also create an `images` folder in your `public` directory and add the image files corresponding to the product data. For example, add `tshirt.jpg`, `jeans.jpg`, etc.
Fetching Product Data
Next, we’ll create a function to fetch our product data. In a real-world application, this might involve fetching data from a database or an external API. For this tutorial, we’ll import our local JSON file.
Create a file named `lib/products.js` and add the following code:
import fs from 'fs';
import path from 'path';
export function getProducts() {
const filePath = path.join(process.cwd(), 'products.json');
const jsonData = fs.readFileSync(filePath, 'utf8');
const data = JSON.parse(jsonData);
return data;
}
This function reads the `products.json` file and parses the JSON data. We will use this function in our pages to retrieve the product data.
Building the Product Listing Page
Now, let’s create the main product listing page. Open `pages/index.js` and replace the contents with the following code:
import { useState } from 'react';
import { getProducts } from '../lib/products';
import { FaFilter } from 'react-icons/fa';
export default function Home({ products }) {
const [filters, setFilters] = useState({});
const filteredProducts = products.filter(product => {
let matches = true;
for (const key in filters) {
if (filters[key].length > 0) {
if (!filters[key].includes(product[key])) {
matches = false;
break;
}
}
}
return matches;
});
const handleFilterChange = (key, value) => {
setFilters(prevFilters => {
const newFilters = { ...prevFilters };
if (newFilters[key]) {
if (newFilters[key].includes(value)) {
newFilters[key] = newFilters[key].filter(item => item !== value);
} else {
newFilters[key] = [...newFilters[key], value];
}
} else {
newFilters[key] = [value];
}
return newFilters;
});
};
const categories = [...new Set(products.map(product => product.category))];
const colors = [...new Set(products.map(product => product.color))];
const sizes = [...new Set(products.map(product => product.size))];
return (
<div>
<header>
<h1>Product Catalog</h1>
</header>
<div>
<button> Filters</button>
<div>
<h2>Category</h2>
{categories.map(category => (
<label>
handleFilterChange('category', category)}
checked={filters.category?.includes(category) || false}
/>
{category}
</label>
))}
<h2>Color</h2>
{colors.map(color => (
<label>
handleFilterChange('color', color)}
checked={filters.color?.includes(color) || false}
/>
{color}
</label>
))}
<h2>Size</h2>
{sizes.map(size => (
<label>
handleFilterChange('size', size)}
checked={filters.size?.includes(size) || false}
/>
{size}
</label>
))}
</div>
</div>
<main>
{filteredProducts.map(product => (
<div>
<img src="{product.image}" alt="{product.name}" />
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
))}
</main>
{`
.container {
padding: 2rem;
}
header {
margin-bottom: 2rem;
}
.filter-section {
margin-bottom: 2rem;
}
.filter-toggle {
background-color: #f0f0f0;
border: none;
padding: 0.5rem 1rem;
cursor: pointer;
border-radius: 4px;
margin-bottom: 1rem;
}
.filter-options {
display: none;
padding: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.filter-options label {
display: block;
margin-bottom: 0.5rem;
}
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
.product-card {
border: 1px solid #ddd;
padding: 1rem;
border-radius: 4px;
}
.product-card img {
width: 100%;
height: 200px;
object-fit: cover;
margin-bottom: 0.5rem;
}
`}
</div>
);
}
export async function getStaticProps() {
const products = getProducts();
return {
props: {
products,
},
};
}
Let’s break down this code:
- Import Statements: We import `useState` from React, `getProducts` from our `lib/products.js` file, and `FaFilter` from `react-icons`.
- `Home` Component: This is our main component. It receives the `products` data as a prop.
- `useState` for Filters: We use the `useState` hook to manage the filter state. The `filters` state is an object where the keys are the filter categories (e.g., ‘category’, ‘color’, ‘size’) and the values are arrays of selected filter options.
- `filteredProducts` Calculation: This part filters the products based on the current `filters` state. It iterates through the products and checks if each product matches the selected filters.
- `handleFilterChange` Function: This function updates the `filters` state when a filter checkbox is clicked. It toggles the selected filter option on or off.
- Extracting Filter Options: We extract the unique categories, colors, and sizes from the product data to populate the filter options in the UI.
- UI Structure: The code renders the following:
- A heading for the product catalog.
- A filter section with filter options (category, color, size).
- A product grid displaying the filtered products.
- `getStaticProps` Function: This Next.js function fetches the product data at build time and passes it as a prop to the `Home` component. This ensures the page is pre-rendered, improving SEO and performance.
Run your Next.js application using `npm run dev` and navigate to `http://localhost:3000` in your browser. You should see a basic product catalog with filter options. Currently, the filter options are shown by default. Let’s add the functionality to show/hide the filter options.
Adding Filter Toggle Functionality
To improve the user experience, let’s add a button to toggle the visibility of the filter options. Modify the `pages/index.js` file as follows:
import { useState } from 'react';
import { getProducts } from '../lib/products';
import { FaFilter } from 'react-icons/fa';
export default function Home({ products }) {
const [filters, setFilters] = useState({});
const [showFilters, setShowFilters] = useState(false); // Add this line
const filteredProducts = products.filter(product => {
let matches = true;
for (const key in filters) {
if (filters[key].length > 0) {
if (!filters[key].includes(product[key])) {
matches = false;
break;
}
}
}
return matches;
});
const handleFilterChange = (key, value) => {
setFilters(prevFilters => {
const newFilters = { ...prevFilters };
if (newFilters[key]) {
if (newFilters[key].includes(value)) {
newFilters[key] = newFilters[key].filter(item => item !== value);
} else {
newFilters[key] = [...newFilters[key], value];
}
} else {
newFilters[key] = [value];
}
return newFilters;
});
};
const categories = [...new Set(products.map(product => product.category))];
const colors = [...new Set(products.map(product => product.color))];
const sizes = [...new Set(products.map(product => product.size))];
return (
<div>
<header>
<h1>Product Catalog</h1>
</header>
<div>
<button> setShowFilters(!showFilters)}> Filters</button> {/* Add onClick handler */}
<div style="{{"> {/* Conditionally render */}
<h2>Category</h2>
{categories.map(category => (
<label>
handleFilterChange('category', category)}
checked={filters.category?.includes(category) || false}
/>
{category}
</label>
))}
<h2>Color</h2>
{colors.map(color => (
<label>
handleFilterChange('color', color)}
checked={filters.color?.includes(color) || false}
/>
{color}
</label>
))}
<h2>Size</h2>
{sizes.map(size => (
<label>
handleFilterChange('size', size)}
checked={filters.size?.includes(size) || false}
/>
{size}
</label>
))}
</div>
</div>
<main>
{filteredProducts.map(product => (
<div>
<img src="{product.image}" alt="{product.name}" />
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
))}
</main>
{`
.container {
padding: 2rem;
}
header {
margin-bottom: 2rem;
}
.filter-section {
margin-bottom: 2rem;
}
.filter-toggle {
background-color: #f0f0f0;
border: none;
padding: 0.5rem 1rem;
cursor: pointer;
border-radius: 4px;
margin-bottom: 1rem;
}
.filter-options {
display: none;
padding: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.filter-options label {
display: block;
margin-bottom: 0.5rem;
}
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
.product-card {
border: 1px solid #ddd;
padding: 1rem;
border-radius: 4px;
}
.product-card img {
width: 100%;
height: 200px;
object-fit: cover;
margin-bottom: 0.5rem;
}
`}
</div>
);
}
export async function getStaticProps() {
const products = getProducts();
return {
props: {
products,
},
};
}
Here’s what changed:
- `showFilters` State: We added a new state variable `showFilters` to control the visibility of the filter options.
- `onClick` Handler: We added an `onClick` handler to the filter toggle button. This handler toggles the `showFilters` state.
- Conditional Rendering: We used the `showFilters` state to conditionally render the filter options using the `style` prop with a ternary operator.
Now, when you click the “Filters” button, the filter options will appear or disappear.
Adding a Clear Filters Button
To enhance usability, let’s add a button to clear all selected filters. Add the following code inside the `filter-section` div in `pages/index.js`:
<button> setFilters({})}>Clear Filters</button>
Add the following CSS in your `style jsx` block:
.clear-filters-button {
background-color: #ddd;
border: none;
padding: 0.5rem 1rem;
cursor: pointer;
border-radius: 4px;
margin-bottom: 1rem;
}
This button resets the `filters` state to an empty object, effectively clearing all filters. Refresh your browser to see the new button.
Adding Product Detail Pages (Dynamic Routes)
To provide more information about each product, let’s add product detail pages. We’ll use Next.js’s dynamic routes to create these pages.
First, create a new folder named `pages/products` in your project. Then, inside the `products` folder, create a file named `[id].js`. This bracketed `[id]` syntax tells Next.js to create a dynamic route where `id` is a parameter that can vary.
Add the following code to `pages/products/[id].js`:
import { getProducts } from '../../lib/products';
export default function ProductDetail({ product }) {
if (!product) {
return <div>Product not found.</div>;
}
return (
<div>
<header>
<h1>{product.name}</h1>
</header>
<div>
<img src="{product.image}" alt="{product.name}" />
<p>{product.description}</p>
<p>Price: ${product.price}</p>
</div>
{`
.container {
padding: 2rem;
}
header {
margin-bottom: 2rem;
}
.product-details {
margin-bottom: 2rem;
}
.product-details img {
width: 100%;
max-width: 400px;
margin-bottom: 1rem;
}
`}
</div>
);
}
export async function getStaticPaths() {
const products = getProducts();
const paths = products.map(product => ({
params: {
id: product.id.toString(),
},
}));
return {
paths,
fallback: false,
};
}
export async function getStaticProps({ params }) {
const products = getProducts();
const product = products.find(product => product.id.toString() === params.id);
return {
props: {
product: product || null,
},
};
}
Let’s break down this code:
- `getStaticPaths` Function: This function is responsible for pre-rendering all possible product detail pages at build time. It fetches all products and generates an array of paths, where each path represents a product ID. The `fallback: false` option means that if a path is not found in the generated paths, a 404 page will be displayed.
- `getStaticProps` Function: This function fetches the product data for a specific product based on the `id` parameter from the URL. It finds the product with the matching ID and passes it as a prop to the `ProductDetail` component. If no product is found, it passes `null`.
- `ProductDetail` Component: This component displays the details of a single product, including its name, image, description, and price. It checks if the product exists and displays an error message if it doesn’t.
Now, let’s modify the `pages/index.js` file to link to these product detail pages. Inside the `product-card` div, add a link to the product detail page:
import Link from 'next/link'; // Import Link
// ... inside the product-card div
<Link href={`/products/${product.id}`}>
<a>
<img src="{product.image}" alt="{product.name}" />
<h3>{product.name}</h3>
<p>${product.price}</p>
</a>
</Link>
Add the following CSS in your `style jsx` block:
.product-link {
text-decoration: none;
color: inherit;
}
Make sure to import the `Link` component from `next/link`. Now, when you click on a product card, you’ll be taken to its detail page. Next.js will pre-render all these pages during the build process.
Common Mistakes and Troubleshooting
Here are some common mistakes and how to fix them:
- Incorrect File Paths: Double-check your file paths, especially when importing local files like `products.json` and image files. Typos in file names or incorrect relative paths are frequent sources of errors.
- Missing Dependencies: Ensure you have installed all the necessary dependencies. Run `npm install` in your project directory to install any missing packages.
- Incorrect Data Structure: Verify that your `products.json` file is correctly formatted and contains the expected data fields (id, name, description, image, price, category, color, size, available). Invalid JSON can cause parsing errors.
- Typographical Errors: Carefully review your code for typos in variable names, function names, and component names. These can lead to unexpected behavior.
- CSS Issues: If your styling is not working as expected, check your CSS for syntax errors, incorrect selectors, or specificity issues. Use your browser’s developer tools to inspect the elements and see if the styles are being applied.
- `getStaticPaths` Problems: If your product detail pages are not rendering, ensure that `getStaticPaths` is correctly generating the paths for all product IDs. Also, check that `fallback: false` is correctly configured.
- Filter Logic Errors: Double-check the logic within the `filteredProducts` calculation and `handleFilterChange` function to ensure that your filtering is working as intended. Use `console.log` statements to debug the filter state and the filtered product data.
- Image Paths: Make sure your image paths in `products.json` are correct and that the images are located in the `public/images` directory.
SEO Best Practices
To optimize your product catalog for search engines, consider these SEO best practices:
- Use Descriptive Titles and Meta Descriptions: The title tag and meta description are crucial for attracting users from search results. Make sure your title tags are concise and relevant, and your meta descriptions accurately summarize the page content.
- Optimize Image Alt Text: Provide descriptive alt text for all product images. This helps search engines understand the content of the images and improves accessibility.
- Use Semantic HTML: Use semantic HTML elements like `<header>`, `<main>`, `<article>`, `<aside>`, and `<footer>` to structure your content. This helps search engines understand the context of your content.
- Keyword Research: Identify relevant keywords for your products and incorporate them naturally into your page titles, headings, descriptions, and content. Avoid keyword stuffing, which can harm your rankings.
- Mobile-Friendly Design: Ensure your product catalog is responsive and works well on all devices. Mobile-friendliness is a ranking factor for search engines.
- Fast Loading Speed: Optimize your website’s loading speed by compressing images, minifying CSS and JavaScript, and using techniques like lazy loading.
- Internal Linking: Link to related products and categories within your website to improve navigation and distribute link equity.
- XML Sitemap: Create and submit an XML sitemap to search engines to help them discover and crawl your pages.
Key Takeaways
This tutorial has provided a practical guide to building a dynamic product catalog with filtering in Next.js. We’ve covered:
- Setting up a Next.js project.
- Fetching and structuring product data.
- Creating a product listing page with filter functionality.
- Implementing dynamic routes for product detail pages.
- Adding filter toggle and clear filter functionalities.
- Addressing common issues and troubleshooting.
- Applying SEO best practices.
FAQ
Q: How can I integrate this with a real database?
A: Instead of importing from `products.json`, you would replace the `getProducts` function with a function that fetches data from your database (e.g., using Prisma, Mongoose, or a similar ORM/ODM). You would also need to configure your database connection and handle authentication if needed.
Q: How can I add pagination to the product listing?
A: You can implement pagination by fetching a subset of products at a time and adding navigation controls (e.g., next/previous buttons, page numbers) to allow users to navigate through the product list. You’ll need to modify the `getStaticProps` function (or `getServerSideProps` if you’re fetching data on the server for each request) to handle pagination parameters (e.g., page number, page size) and fetch the appropriate data from your data source.
Q: How can I improve the performance of the filtering?
A: For large product catalogs, consider these optimizations: use memoization to cache the results of expensive calculations, implement server-side filtering and pagination, and use techniques like debouncing or throttling to limit the frequency of filter updates. Consider using a state management library like Zustand or Redux for managing complex application state.
Q: How can I deploy this application?
A: Next.js applications can be deployed to various platforms, including Vercel (recommended, as it’s built by the creators of Next.js), Netlify, AWS, and others. You’ll typically build your application using `npm run build` and then deploy the generated static files to your chosen hosting platform.
The journey of building a dynamic product catalog in Next.js is a rewarding one. From the initial setup to the implementation of filtering and dynamic routing, you’ve gained valuable skills in modern web development. By following this tutorial, you’ve not only built a functional application but have also learned best practices for performance, SEO, and user experience. Remember that the key to mastering any technology is practice and exploration. Experiment with different features, explore alternative approaches, and don’t be afraid to make mistakes. The more you build, the more confident you’ll become in your ability to create compelling and effective web applications.
