Atlas CMS
Use Cases

Product Catalog

Build a product catalog with filtering, search, and pagination using the Atlas Public API.

Product Catalog & E-Commerce

A classic use case for a Headless CMS is managing a product catalog. In this guide, we'll build a product listing page that supports category filtering and searching using the Atlas CMS Public API.

Schema Setup

Assume you have a product content type in Atlas with fields like name, description, price, category (dropdown/string), and image.

What We're Building

We will create a Next.js Server Component that reads query parameters (e.g., ?category=shoes&q=sneaker) from the URL and passes them to the Atlas API for filtering and search.


1. API Helper

We need a function to fetch products that accepts filtering parameters.

lib/atlas.ts
const BASE = process.env.ATLAS_BASE_URL + '/api/v1/public'

const headers = {
  'X-API-Key': process.env.ATLAS_API_KEY!,
}

export interface Product {
  slug: string
  data: {
    name: string
    description?: string
    price: number
    category: string
    image?: { url: string; width: number; height: number; alt?: string }
  }
}

export async function listProducts(options: {
  page?: number
  category?: string
  search?: string
}): Promise<{
  data: Product[]
  meta: { total: number }
}> {
  const url = new URL(`${BASE}/entries`)
  url.searchParams.append('type', 'product')
  url.searchParams.append('limit', '12')
  url.searchParams.append('page', String(options.page || 1))

  // Exact match filter for category
  if (options.category) {
    url.searchParams.append('filter[category]', options.category)
  }

  // Search across text fields (e.g., name, description)
  if (options.search) {
    url.searchParams.append('search', options.search)
  }

  const res = await fetch(url.toString(), { headers, next: { revalidate: 60 } })
  if (!res.ok) return { data: [], meta: { total: 0 } }
  return res.json()
}

2. Product Listing Page

We'll build a page that reads searchParams and passes them to our API helper.

app/products/page.tsx
import Image from 'next/image'
import Link from 'next/link'
import { listProducts } from '@/lib/atlas'

type Props = {
  searchParams: Promise<{ category?: string; q?: string; page?: string }>
}

export default async function ProductsPage({ searchParams }: Props) {
  const { category, q, page } = await searchParams
  const currentPage = Number(page || 1)

  const { data: products, meta } = await listProducts({
    category,
    search: q,
    page: currentPage,
  })

  return (
    <main className="container py-12">
      <h1 className="text-3xl font-bold mb-8">Our Products</h1>

      {/* Filter Controls */}
      <div className="flex gap-4 mb-8">
        <Link href="/products" className={`px-4 py-2 border rounded ${!category ? 'bg-black text-white' : ''}`}>
          All
        </Link>
        <Link href="/products?category=shoes" className={`px-4 py-2 border rounded ${category === 'shoes' ? 'bg-black text-white' : ''}`}>
          Shoes
        </Link>
        <Link href="/products?category=apparel" className={`px-4 py-2 border rounded ${category === 'apparel' ? 'bg-black text-white' : ''}`}>
          Apparel
        </Link>
      </div>

      <p className="text-sm text-muted mb-6">Showing {products.length} of {meta.total} products</p>

      {/* Product Grid */}
      <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
        {products.map((product) => (
          <div key={product.slug} className="border rounded-lg p-4 group">
            {product.data.image && (
              <Image
                src={product.data.image.url}
                alt={product.data.image.alt ?? product.data.name}
                width={product.data.image.width}
                height={product.data.image.height}
                className="rounded-lg mb-4 w-full object-cover aspect-square bg-gray-100 group-hover:scale-105 transition-transform"
              />
            )}
            <h2 className="text-lg font-semibold">{product.data.name}</h2>
            <p className="text-muted">${product.data.price.toFixed(2)}</p>
          </div>
        ))}
      </div>
    </main>
  )
}

Next Steps

  • Implement client-side forms for the search input to update the q query parameter.
  • Add Pagination controls at the bottom of the grid.

On this page