Product Listing Page (PLP)
Collection and search results pages with filtering, sorting, and infinite scroll.
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 infinite scroll components.
Product grid
Products display in a responsive grid: 2 columns on mobile, 3 on tablet, 4 on desktop. During filter or sort transitions, a pending overlay dims the grid to signal loading.
Each product renders as a card with the featured image (with hover slideshow on desktop), title, price, compare-at price, and availability status.
Filtering
A filter sidebar provides multi-select faceted filters for:
- Size, Color, Vendor, Product Type, Tags - checkbox-based multi-select
- Price range - slider with 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 triggered from the toolbar.
The filter UI primitives (checkboxes, price slider, section headers) are extracted into filter-primitives.tsx for reuse, while the async server-side filter data fetching lives in filter-sidebar.tsx.
Sorting
A select dropdown offers sort options. Collection pages support all eight:
- Best Matches (default)
- Best Selling
- Name: A to Z
- Name: Z to A
- Price: Low to High
- Price: High to Low
- Date: Old to New
- Date: New to Old
The search page excludes name and date sorts because Shopify's SearchSortKeys does not support them.
Sort changes use useTransition for non-blocking navigation.
Infinite scroll
Products load progressively using an InfiniteProductGrid component backed by IntersectionObserver. When the user scrolls near the bottom, a server action fetches the next page using Shopify's cursor-based pagination (endCursor). A loading indicator appears while the next batch loads.
The cursor resets automatically when filters or sort change. Each batch renders product cards inline using the same card primitives as the initial grid.
Toolbar
A CollectionToolbar component sits above the grid and contains:
- Filter sheet trigger — opens the filter sidebar (with active filter count badge)
- Result count — visible on desktop, hidden on mobile
- Sort dropdown — the sort select control
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, sort, and infinite scroll controls. The search page displays a heading with the query and shows a "no results" message with i18n support when the query returns empty. Predictive search is available from the navbar search modal, which shows suggestion chips and product results as the user types.
Key files
| File | Purpose |
|---|---|
app/collections/[handle]/page.tsx | Collection route |
app/search/page.tsx | Search route |
components/collections/collection-page.tsx | Orchestrates toolbar, filter sidebar, results grid, and JSON-LD |
components/collections/infinite-product-grid.tsx | Infinite scroll via IntersectionObserver + server action |
components/collections/filter-sidebar.tsx | Faceted filter sidebar driven by Shopify filter data |
lib/shopify/operations/products.ts | Product search with caching and filter mapping |
The toolbar, sort select, mobile filter sheet, filter primitives, transition context, search results layout, and product card live alongside these in components/collections/, components/search/, and components/product-card/.