Product Listing Page (PLP)
Collection and search results pages with filtering, sorting, and pagination.
The PLP covers two routes - /collections/[handle] for browsing a collection and /search for query-based product search. Both share the same grid, filtering, sorting, and pagination components.
Product grid
Products display in a responsive grid: 2 columns on mobile, 3 on tablet, 4 on desktop. Each page shows up to 50 products. During filter or sort transitions, a ProductGridPendingOverlay dims the grid to signal loading.
Each product renders as a card with the featured image, title, price, compare-at price, availability status, and a quick-add button for in-stock items.
Filtering
A sidebar provides multi-select faceted filters for:
- Size, Color, Vendor, Product Type, Tags - checkbox-based multi-select
- Price range - slider with 4 presets (Under $50, $50–$100, $100–$200, Over $200)
Active filters appear as badges with remove buttons. Filter state is encoded in URL search params using Shopify's standard format (?filter.v.option.color=red&filter.v.price.gte=10&filter.v.price.lte=100) so filtered views are shareable and compatible with Shopify themes.
Filters use useOptimistic for instant UI feedback while the server request is in flight, coordinated through a FilterTransitionProvider context.
On mobile, filters are accessible through a sheet overlay rather than a persistent sidebar.
Sorting
A select dropdown offers five sort options:
- Best Matches (default)
- Price: Low to High
- Price: High to Low
- Name: A to Z
- Name: Z to A
Sort changes use useTransition for non-blocking navigation. On mobile, the sort control renders as a bottom sheet.
Pagination
Pagination is cursor-based using Shopify's endCursor rather than page numbers. A First/Next button pair navigates between pages, with loading indicators and disabled states at boundaries. The cursor resets automatically when filters or sort change.
Collection pages
The /collections/[handle] route fetches collection metadata (title, description, image, SEO) and its products in parallel. Collection data is cached with cacheLife("max") and tagged for granular revalidation (collection-{handle}).
Products within a collection are fetched via getCollectionProducts(), which accepts Shopify ProductFilter[] for server-side filtering by variant options, price, vendor, type, tags, and metafields.
Note: Filters are driven entirely by Shopify's
ProductFiltertype. Adding custom filter logic (e.g., filtering by a metafield) requires modifying the GraphQL query inlib/shopify/operations/products.ts.
Search page
The /search route accepts a q query parameter and combines it with the same filter and sort controls. getProducts() builds a Shopify search query from the text input and active filters, mapping UI sort keys to Shopify's RELEVANCE and PRICE sort keys.
The search page displays a custom header with the query and result count, and shows a "no results" message with i18n support when the query returns empty.
Key files
| File | Purpose |
|---|---|
app/collections/[handle]/page.tsx | Collection route with parallel data fetching |
app/search/page.tsx | Search route with query param handling |
components/collections/results-grid.tsx | Responsive product grid with loading overlay |
components/filters/collection-filter-sidebar.tsx | Faceted filter sidebar with optimistic UI |
components/collections/sort-select.tsx | Sort dropdown (desktop) / bottom sheet (mobile) |
components/collections/pagination.tsx | Cursor-based pagination controls |
components/product-card.tsx | Product card with image, hover slideshow, price, quick-add |
components/search/results.tsx | Search results layout with no-results state |
lib/shopify/operations/products.ts | Product search with caching and filter mapping |
lib/shopify/operations/collections.ts | Collection metadata fetching |