Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
709f380
Add collection and search filtering & sorting to skeleton template
danielqiuu Mar 24, 2026
8657624
Rename lib files to kebab-case per contributing guidelines
danielqiuu Mar 24, 2026
a6c14d7
Rewrite changeset for merchant audience
danielqiuu Mar 24, 2026
302ef78
Add unit tests for product-filters and product-sort utilities
danielqiuu Mar 24, 2026
557350d
Remove skeleton test files
danielqiuu Mar 24, 2026
1b12fae
Update search guide to document filtering and sorting
danielqiuu Mar 24, 2026
5c5c052
Fix formatting in search guide
danielqiuu Mar 24, 2026
875eed1
Regenerate cookbook recipe patches for skeleton changes
danielqiuu Mar 25, 2026
ca17409
Revert "Regenerate cookbook recipe patches for skeleton changes"
danielqiuu Mar 25, 2026
27c3de6
Reapply "Regenerate cookbook recipe patches for skeleton changes"
danielqiuu Mar 25, 2026
465f331
Fix cookbook recipe patches and deletedFiles for skeleton changes
danielqiuu Mar 25, 2026
69cbd0c
Restore infinite-scroll package.json patch to match main
danielqiuu Mar 25, 2026
3fa7019
Fix express package.json patch: use catalog:/workspace:* context, upd…
danielqiuu Mar 25, 2026
68eace8
Add e2e tests for collection and search filtering & sorting
danielqiuu Mar 26, 2026
12d61eb
Add console error checks and product rendering assertions to e2e tests
danielqiuu Mar 26, 2026
56f61c7
Fix e2e test failures
danielqiuu Mar 26, 2026
79baaf2
Rename from Collection to Product
danielqiuu Apr 8, 2026
de9190c
Update tests to use role based locators
danielqiuu Apr 8, 2026
bb481e5
Fix search.md formatting
danielqiuu Apr 8, 2026
33546fb
Update search test to assert for articles
danielqiuu Apr 8, 2026
f11aeb8
Add runtime validation for JSON casting
danielqiuu Apr 9, 2026
a7cd9b0
Extract shared logic for checking if a filter is active
danielqiuu Apr 10, 2026
54018fd
Replace hardcoded sort-select id with react useId
danielqiuu Apr 10, 2026
627ca44
Remove sort by aria label and update E2E tests to use visual label
danielqiuu Apr 10, 2026
e94c343
Add a central utility to navigate for filters
danielqiuu Apr 10, 2026
2145db5
use a defaultKey parameter for default sorting
danielqiuu Apr 10, 2026
4a21273
Add basic sanitation for parsing min and max values
danielqiuu Apr 10, 2026
fc7b382
Remove inline styles
danielqiuu Apr 10, 2026
dd4f0e1
Wire maxprice into price range filter inputs
danielqiuu Apr 10, 2026
491ffc8
Fix typescript error
danielqiuu Apr 10, 2026
6f08502
Cookbook patch fixes
danielqiuu Apr 10, 2026
c80b2f6
Fix search tests
danielqiuu Apr 10, 2026
ac9d255
Update search test
danielqiuu Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/collection-search-filtering-sorting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"skeleton": patch
"@shopify/cli-hydrogen": patch
"@shopify/create-hydrogen": patch
---

Add collection and search filtering & sorting to the skeleton template

- **Collection filtering**: Collection pages now display product filters (list, color/image swatches, and price range) that your customers can use to narrow results. Filters are driven by the options you configure in the Shopify admin under **Online Store → Navigation → Collection and search filters**.
- **Collection sorting**: A sort dropdown is now available on collection pages with options for Featured, Price, Best Selling, Alphabetical, and Date.
- **Search sorting & filtering**: The search results page now supports the same product filters and sort options (Relevance, Price).
- **Bug fix**: Article links in search results now navigate to the correct URL.
5 changes: 3 additions & 2 deletions cookbook/recipes/bundles/patches/app.css.dedf0f.patch
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
index 3be08d31..51b74911 100644
diff --git a/templates/skeleton/app/styles/app.css b/templates/skeleton/app/styles/app.css
index 86c54680..244e81d7 100644
--- a/templates/skeleton/app/styles/app.css
+++ b/templates/skeleton/app/styles/app.css
@@ -506,6 +506,10 @@ button.reset:hover:not(:has(> *)) {
@@ -684,6 +684,10 @@ button.reset:hover:not(:has(> *)) {
margin-top: 0;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
index c416c2b3..773ba8b6 100644
diff --git a/templates/skeleton/app/routes/collections.$handle.tsx b/templates/skeleton/app/routes/collections.$handle.tsx
index d6285de8..57701d2b 100644
--- a/templates/skeleton/app/routes/collections.$handle.tsx
+++ b/templates/skeleton/app/routes/collections.$handle.tsx
@@ -120,10 +120,16 @@ const PRODUCT_ITEM_FRAGMENT = `#graphql
@@ -143,10 +143,16 @@ const PRODUCT_ITEM_FRAGMENT = `#graphql
...MoneyProductItem
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
index 3be08d31..38e9afed 100644
diff --git a/templates/skeleton/app/styles/app.css b/templates/skeleton/app/styles/app.css
index 86c54680..42928dce 100644
--- a/templates/skeleton/app/styles/app.css
+++ b/templates/skeleton/app/styles/app.css
@@ -489,6 +489,11 @@ button.reset:hover:not(:has(> *)) {
@@ -667,6 +667,11 @@ button.reset:hover:not(:has(> *)) {
width: 100%;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,58 +1,12 @@
index c416c2b3..b627a950 100644
diff --git a/templates/skeleton/app/routes/collections.$handle.tsx b/templates/skeleton/app/routes/collections.$handle.tsx
index d6285de8..5307112d 100644
--- a/templates/skeleton/app/routes/collections.$handle.tsx
+++ b/templates/skeleton/app/routes/collections.$handle.tsx
@@ -5,6 +5,10 @@ import {PaginatedResourceSection} from '~/components/PaginatedResourceSection';
import {redirectIfHandleIsLocalized} from '~/lib/redirect';
import {ProductItem} from '~/components/ProductItem';
import type {ProductItemFragment} from 'storefrontapi.generated';
+import {
+ combinedListingsSettings,
+ isCombinedListing,
+} from '~/lib/combined-listings';

export const meta: Route.MetaFunction = ({data}) => {
return [{title: `Hydrogen | ${data?.collection.title ?? ''} Collection`}];
@@ -68,12 +72,25 @@ function loadDeferredData({context}: Route.LoaderArgs) {
export default function Collection() {
const {collection} = useLoaderData<typeof loader>();

+ // Manually filter out combined listings from the collection products, because filtering
+ // would not work here.
+ const filteredCollectionProducts = {
+ ...collection.products,
+ nodes: collection.products.nodes.filter(
+ (product) =>
+ !(
+ combinedListingsSettings.hideCombinedListingsFromProductList &&
+ isCombinedListing(product)
+ ),
+ ),
+ };
+
return (
<div className="collection">
<h1>{collection.title}</h1>
<p className="collection-description">{collection.description}</p>
<PaginatedResourceSection<ProductItemFragment>
- connection={collection.products}
+ connection={filteredCollectionProducts}
resourcesClassName="products-grid"
>
{({node: product, index}) => (
@@ -105,6 +122,7 @@ const PRODUCT_ITEM_FRAGMENT = `#graphql
@@ -128,6 +128,7 @@ const PRODUCT_ITEM_FRAGMENT = `#graphql
id
handle
title
+ tags
featuredImage {
id
altText
@@ -144,7 +162,7 @@ const COLLECTION_QUERY = `#graphql
first: $first,
last: $last,
before: $startCursor,
- after: $endCursor
+ after: $endCursor,
) {
nodes {
...ProductItem
192 changes: 185 additions & 7 deletions cookbook/recipes/express/patches/app.css.dedf0f.patch
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
index 3be08d31..98fde229 100644
diff --git a/templates/skeleton/app/styles/app.css b/templates/skeleton/app/styles/app.css
index 86c54680..98fde229 100644
--- a/templates/skeleton/app/styles/app.css
+++ b/templates/skeleton/app/styles/app.css
@@ -1,645 +1,54 @@
@@ -1,818 +1,54 @@
-:root {
- --aside-width: 400px;
- --cart-aside-summary-height-with-discount: 300px;
Expand Down Expand Up @@ -502,18 +503,195 @@ index 3be08d31..98fde229 100644
+ line-height: 1.4;
}

-.products-grid {
- display: grid;
- grid-gap: 1.5rem;
- grid-template-columns: repeat(auto-fit, minmax(var(--grid-item-width), 1fr));
-.product-controls {
- margin-bottom: 1.5rem;
- display: flex;
- align-items: center;
- justify-content: flex-end;
-}
-
-/*
-* --------------------------------------------------
-* components/ProductSort
-* --------------------------------------------------
-*/
-.product-sort {
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.product-sort label {
- font-size: 0.875rem;
- color: #666;
-}
-
-.product-sort select {
- padding: 0.5rem 2rem 0.5rem 0.75rem;
- border: 1px solid #ccc;
- border-radius: 4px;
- background: white;
- cursor: pointer;
- font-size: 0.875rem;
- appearance: none;
- background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L6 6L11 1' stroke='%23333' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
- background-repeat: no-repeat;
- background-position: right 0.5rem center;
-}
-
-.product-sort select:hover {
- border-color: #999;
-}
-
-.product-sort select:focus {
- outline: 2px solid black;
- outline-offset: 2px;
-}
-
-/*
-* --------------------------------------------------
-* components/ProductFilters
-* --------------------------------------------------
-*/
-.product-filters {
- margin-bottom: 2rem;
-}
-
-.product-filters-clear {
- padding: 0.5rem 1rem;
- background: black;
- color: white;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-size: 0.875rem;
+h2 {
+ font-size: 1.2rem;
+ font-weight: 700;
+ margin-bottom: 1rem;
margin-bottom: 1rem;
+ line-height: 1.4;
}

-.product-filters-clear:hover {
- background: #333;
-}
-
-.product-filter-group {
- margin-bottom: 1.5rem;
-}
-
-.product-filter-group h3 {
- margin-bottom: 0.5rem;
-}
-
-.product-filter-options {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5rem;
-}
-
-.product-filter-option {
- padding: 0.5rem 1rem;
- cursor: pointer;
- background: white;
- border: 1px solid #ccc;
- border-radius: 4px;
-}
-
-.product-filter-option[aria-pressed='true'] {
- border: 2px solid black;
-}
-
-.product-filter-option.has-swatch {
- padding: 0.375rem;
- min-width: 3rem;
- min-height: 3rem;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.swatch-color,
-.swatch-image {
- width: 2rem;
- height: 2rem;
- border-radius: 50%;
- display: block;
-}
-
-.swatch-color {
- border: 1px solid rgba(0, 0, 0, 0.1);
-}
-
-.swatch-image {
- object-fit: cover;
-}
-
-/*
-* --------------------------------------------------
-* components/PriceRangeFilter
-* --------------------------------------------------
-*/
-.price-range-filter {
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
-}
-
-.price-inputs {
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.price-inputs input {
- padding: 0.5rem;
- border: 1px solid #ccc;
- border-radius: 4px;
- width: 5rem;
- font-size: 0.875rem;
-}
-
-.price-separator {
- color: #666;
- font-size: 0.875rem;
-}
-
-.price-actions {
- display: flex;
- gap: 0.5rem;
-}
-
-.price-actions button {
- padding: 0.5rem 1rem;
- border: 1px solid #ccc;
- border-radius: 4px;
- background: white;
- cursor: pointer;
- font-size: 0.875rem;
-}
-
-.price-actions button:hover {
- background: #f5f5f5;
-}
-
-.price-actions button:first-child {
- background: black;
- color: white;
- border-color: black;
-}
-
-.price-actions button:first-child:hover {
- background: #333;
-}
-
-.products-grid {
- display: grid;
- grid-gap: 1.5rem;
- grid-template-columns: repeat(auto-fit, minmax(var(--grid-item-width), 1fr));
- margin-bottom: 2rem;
-}
-
-.product-item img {
- height: auto;
- width: 100%;
Expand Down
4 changes: 2 additions & 2 deletions cookbook/recipes/express/patches/package.json.acbf33.patch
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ index e971ba7e..ae934ffd 100644
--- a/templates/skeleton/package.json
+++ b/templates/skeleton/package.json
@@ -5,59 +5,52 @@
"version": "2026.1.1",
"version": "2026.1.2",
"type": "module",
"scripts": {
- "build": "shopify hydrogen build --codegen",
Expand Down Expand Up @@ -41,7 +41,7 @@ index e971ba7e..ae934ffd 100644
"@graphql-codegen/cli": "5.0.2",
"@react-router/dev": "7.12.0",
"@react-router/fs-routes": "7.12.0",
"@shopify/cli": "3.85.4",
"@shopify/cli": "3.91.1",
"@shopify/hydrogen-codegen": "workspace:*",
- "@shopify/mini-oxygen": "workspace:*",
- "@shopify/oxygen-workers-types": "^4.1.6",
Expand Down
5 changes: 5 additions & 0 deletions cookbook/recipes/express/recipe.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ deletedFiles:
- templates/skeleton/app/components/SearchFormPredictive.tsx
- templates/skeleton/app/components/SearchResults.tsx
- templates/skeleton/app/components/SearchResultsPredictive.tsx
- templates/skeleton/app/components/ProductFilters.tsx
- templates/skeleton/app/components/ProductSort.tsx
- templates/skeleton/app/components/PriceRangeFilter.tsx
- templates/skeleton/app/graphql/customer-account/CustomerAddressMutations.ts
- templates/skeleton/app/graphql/customer-account/CustomerDetailsQuery.ts
- templates/skeleton/app/graphql/customer-account/CustomerOrderQuery.ts
Expand All @@ -66,6 +69,8 @@ deletedFiles:
- templates/skeleton/app/lib/search.ts
- templates/skeleton/app/lib/session.ts
- templates/skeleton/app/lib/variants.ts
- templates/skeleton/app/lib/product-filters.ts
- templates/skeleton/app/lib/product-sort.ts
- templates/skeleton/app/routes/$.tsx
- templates/skeleton/app/routes/[robots.txt].tsx
- templates/skeleton/app/routes/[sitemap.xml].tsx
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
index c416c2b3..e6a35150 100644
diff --git a/templates/skeleton/app/routes/collections.$handle.tsx b/templates/skeleton/app/routes/collections.$handle.tsx
index d6285de8..98c6081c 100644
--- a/templates/skeleton/app/routes/collections.$handle.tsx
+++ b/templates/skeleton/app/routes/collections.$handle.tsx
@@ -1,9 +1,14 @@
Expand All @@ -17,18 +18,20 @@ index c416c2b3..e6a35150 100644
+import {useEffect} from 'react';
+import {useInView} from 'react-intersection-observer';
import type {ProductItemFragment} from 'storefrontapi.generated';

export const meta: Route.MetaFunction = ({data}) => {
@@ -67,23 +72,41 @@ function loadDeferredData({context}: Route.LoaderArgs) {
import {parseFiltersFromParams} from '~/lib/product-filters';
import {parseSortParam, COLLECTION_SORT_OPTIONS} from '~/lib/product-sort';
@@ -84,6 +89,7 @@ function loadDeferredData({context}: Route.LoaderArgs) {

export default function Collection() {
const {collection} = useLoaderData<typeof loader>();
+ const {ref, inView} = useInView();

return (
<div className="collection">
<h1>{collection.title}</h1>
<p className="collection-description">{collection.description}</p>
@@ -95,18 +101,35 @@ export default function Collection() {
{collection.products?.filters && (
<ProductFilters filters={collection.products.filters} />
)}
- <PaginatedResourceSection<ProductItemFragment>
- connection={collection.products}
- resourcesClassName="products-grid"
Expand Down Expand Up @@ -72,7 +75,7 @@ index c416c2b3..e6a35150 100644
<Analytics.CollectionView
data={{
collection: {
@@ -96,6 +119,47 @@ export default function Collection() {
@@ -119,6 +142,47 @@ export default function Collection() {
);
}

Expand Down
Loading
Loading