Skip to content

Commit 76e34b6

Browse files
jorgemoyaclaude
andcommitted
feat(core): wrap faceted search and compare guest queries in unstable_cache
Wrap brand, category, search, compare, and quick search queries in unstable_cache for guest visitors. Faceted search uses 300s revalidation; others use the default interval. Authenticated requests bypass the cache. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent d0e18c3 commit 76e34b6

12 files changed

Lines changed: 284 additions & 70 deletions

File tree

.changeset/guest-cache-faceted.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@bigcommerce/catalyst-core": minor
3+
---
4+
5+
Wrap faceted search, brand, category, compare, and quick search guest queries in `unstable_cache` with configurable revalidation. Authenticated requests continue to bypass the cache.

core/app/[locale]/(default)/(faceted)/brand/[slug]/page-data.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { unstable_cache } from 'next/cache';
12
import { cache } from 'react';
23

34
import { client } from '~/client';
@@ -38,13 +39,35 @@ const BrandPageQuery = graphql(`
3839
}
3940
`);
4041

41-
export const getBrandPageData = cache(async (entityId: number, customerAccessToken?: string) => {
42-
const response = await client.fetch({
43-
document: BrandPageQuery,
44-
variables: { entityId },
45-
customerAccessToken,
46-
fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },
47-
});
42+
const getCachedBrandPageData = unstable_cache(
43+
async (locale: string, entityId: number) => {
44+
const response = await client.fetch({
45+
document: BrandPageQuery,
46+
variables: { entityId },
47+
locale,
48+
fetchOptions: { cache: 'no-store' },
49+
});
4850

49-
return response.data.site;
50-
});
51+
return response.data.site;
52+
},
53+
['brand-page-data'],
54+
{ revalidate },
55+
);
56+
57+
export const getBrandPageData = cache(
58+
async (locale: string, entityId: number, customerAccessToken?: string) => {
59+
if (customerAccessToken) {
60+
const response = await client.fetch({
61+
document: BrandPageQuery,
62+
variables: { entityId },
63+
customerAccessToken,
64+
locale,
65+
fetchOptions: { cache: 'no-store' },
66+
});
67+
68+
return response.data.site;
69+
}
70+
71+
return getCachedBrandPageData(locale, entityId);
72+
},
73+
);

core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,14 @@ const getCachedBrand = cache((brandId: string) => {
3030
const compareLoader = createCompareLoader();
3131

3232
const createBrandSearchParamsLoader = cache(
33-
async (brandId: string, customerAccessToken?: string) => {
33+
async (locale: string, brandId: string, customerAccessToken?: string) => {
3434
const cachedBrand = getCachedBrand(brandId);
35-
const brandSearch = await fetchFacetedSearch(cachedBrand, undefined, customerAccessToken);
35+
const brandSearch = await fetchFacetedSearch(
36+
locale,
37+
cachedBrand,
38+
undefined,
39+
customerAccessToken,
40+
);
3641
const brandFacets = brandSearch.facets.items.filter(
3742
(facet) => facet.__typename !== 'BrandSearchFilter',
3843
);
@@ -73,7 +78,7 @@ export async function generateMetadata(props: Props): Promise<Metadata> {
7378

7479
const brandId = Number(slug);
7580

76-
const { brand } = await getBrandPageData(brandId, customerAccessToken);
81+
const { brand } = await getBrandPageData(locale, brandId, customerAccessToken);
7782

7883
if (!brand) {
7984
return notFound();
@@ -99,7 +104,7 @@ export default async function Brand(props: Props) {
99104

100105
const brandId = Number(slug);
101106

102-
const { brand, settings } = await getBrandPageData(brandId, customerAccessToken);
107+
const { brand, settings } = await getBrandPageData(locale, brandId, customerAccessToken);
103108

104109
if (!brand) {
105110
return notFound();
@@ -114,10 +119,11 @@ export default async function Brand(props: Props) {
114119
const searchParams = await props.searchParams;
115120
const currencyCode = await getPreferredCurrencyCode();
116121

117-
const loadSearchParams = await createBrandSearchParamsLoader(slug, customerAccessToken);
122+
const loadSearchParams = await createBrandSearchParamsLoader(locale, slug, customerAccessToken);
118123
const parsedSearchParams = loadSearchParams?.(searchParams) ?? {};
119124

120125
const search = await fetchFacetedSearch(
126+
locale,
121127
{
122128
...searchParams,
123129
...parsedSearchParams,
@@ -162,10 +168,15 @@ export default async function Brand(props: Props) {
162168

163169
const streamableFilters = Streamable.from(async () => {
164170
const searchParams = await props.searchParams;
165-
const loadSearchParams = await createBrandSearchParamsLoader(slug, customerAccessToken);
171+
const loadSearchParams = await createBrandSearchParamsLoader(locale, slug, customerAccessToken);
166172
const parsedSearchParams = loadSearchParams?.(searchParams) ?? {};
167173
const cachedBrand = getCachedBrand(slug);
168-
const categorySearch = await fetchFacetedSearch(cachedBrand, undefined, customerAccessToken);
174+
const categorySearch = await fetchFacetedSearch(
175+
locale,
176+
cachedBrand,
177+
undefined,
178+
customerAccessToken,
179+
);
169180
const refinedSearch = await streamableFacetedSearch;
170181

171182
const allFacets = categorySearch.facets.items.filter(
@@ -195,7 +206,7 @@ export default async function Brand(props: Props) {
195206

196207
const compareIds = { entityIds: compare ? compare.map((id: string) => Number(id)) : [] };
197208

198-
const products = await getCompareProductsData(compareIds, customerAccessToken);
209+
const products = await getCompareProductsData(locale, compareIds, customerAccessToken);
199210

200211
return products.map((product) => ({
201212
id: product.entityId.toString(),

core/app/[locale]/(default)/(faceted)/category/[slug]/page-data.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { unstable_cache } from 'next/cache';
12
import { cache } from 'react';
23

34
import { client } from '~/client';
@@ -58,13 +59,35 @@ const CategoryPageQuery = graphql(
5859
[BreadcrumbsCategoryFragment],
5960
);
6061

61-
export const getCategoryPageData = cache(async (entityId: number, customerAccessToken?: string) => {
62-
const response = await client.fetch({
63-
document: CategoryPageQuery,
64-
variables: { entityId },
65-
customerAccessToken,
66-
fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },
67-
});
62+
const getCachedCategoryPageData = unstable_cache(
63+
async (locale: string, entityId: number) => {
64+
const response = await client.fetch({
65+
document: CategoryPageQuery,
66+
variables: { entityId },
67+
locale,
68+
fetchOptions: { cache: 'no-store' },
69+
});
6870

69-
return response.data.site;
70-
});
71+
return response.data.site;
72+
},
73+
['category-page-data'],
74+
{ revalidate },
75+
);
76+
77+
export const getCategoryPageData = cache(
78+
async (locale: string, entityId: number, customerAccessToken?: string) => {
79+
if (customerAccessToken) {
80+
const response = await client.fetch({
81+
document: CategoryPageQuery,
82+
variables: { entityId },
83+
customerAccessToken,
84+
locale,
85+
fetchOptions: { cache: 'no-store' },
86+
});
87+
88+
return response.data.site;
89+
}
90+
91+
return getCachedCategoryPageData(locale, entityId);
92+
},
93+
);

core/app/[locale]/(default)/(faceted)/category/[slug]/page.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,14 @@ const getCachedCategory = cache((categoryId: number) => {
3232
const compareLoader = createCompareLoader();
3333

3434
const createCategorySearchParamsLoader = cache(
35-
async (categoryId: number, customerAccessToken?: string) => {
35+
async (locale: string, categoryId: number, customerAccessToken?: string) => {
3636
const cachedCategory = getCachedCategory(categoryId);
37-
const categorySearch = await fetchFacetedSearch(cachedCategory, undefined, customerAccessToken);
37+
const categorySearch = await fetchFacetedSearch(
38+
locale,
39+
cachedCategory,
40+
undefined,
41+
customerAccessToken,
42+
);
3843
const categoryFacets = categorySearch.facets.items.filter(
3944
(facet) => facet.__typename !== 'CategorySearchFilter',
4045
);
@@ -75,7 +80,7 @@ export async function generateMetadata(props: Props): Promise<Metadata> {
7580

7681
const categoryId = Number(slug);
7782

78-
const { category } = await getCategoryPageData(categoryId, customerAccessToken);
83+
const { category } = await getCategoryPageData(locale, categoryId, customerAccessToken);
7984

8085
if (!category) {
8186
return notFound();
@@ -107,6 +112,7 @@ export default async function Category(props: Props) {
107112
const categoryId = Number(slug);
108113

109114
const { category, settings, categoryTree } = await getCategoryPageData(
115+
locale,
110116
categoryId,
111117
customerAccessToken,
112118
);
@@ -130,12 +136,14 @@ export default async function Category(props: Props) {
130136
const currencyCode = await getPreferredCurrencyCode();
131137

132138
const loadSearchParams = await createCategorySearchParamsLoader(
139+
locale,
133140
categoryId,
134141
customerAccessToken,
135142
);
136143
const parsedSearchParams = loadSearchParams?.(searchParams) ?? {};
137144

138145
const search = await fetchFacetedSearch(
146+
locale,
139147
{
140148
...searchParams,
141149
...parsedSearchParams,
@@ -182,12 +190,18 @@ export default async function Category(props: Props) {
182190
const searchParams = await props.searchParams;
183191

184192
const loadSearchParams = await createCategorySearchParamsLoader(
193+
locale,
185194
categoryId,
186195
customerAccessToken,
187196
);
188197
const parsedSearchParams = loadSearchParams?.(searchParams) ?? {};
189198
const cachedCategory = getCachedCategory(categoryId);
190-
const categorySearch = await fetchFacetedSearch(cachedCategory, undefined, customerAccessToken);
199+
const categorySearch = await fetchFacetedSearch(
200+
locale,
201+
cachedCategory,
202+
undefined,
203+
customerAccessToken,
204+
);
191205
const refinedSearch = await streamableFacetedSearch;
192206

193207
const allFacets = categorySearch.facets.items.filter(
@@ -234,7 +248,7 @@ export default async function Category(props: Props) {
234248

235249
const compareIds = { entityIds: compare ? compare.map((id: string) => Number(id)) : [] };
236250

237-
const products = await getCompareProducts(compareIds, customerAccessToken);
251+
const products = await getCompareProducts(locale, compareIds, customerAccessToken);
238252

239253
return products.map((product) => ({
240254
id: product.entityId.toString(),

core/app/[locale]/(default)/(faceted)/fetch-compare-products.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';
22
import { VariablesOf } from 'gql.tada';
3+
import { unstable_cache } from 'next/cache';
34
import { cache } from 'react';
45
import { z } from 'zod';
56

@@ -42,8 +43,8 @@ const CompareProductsQuery = graphql(`
4243

4344
type Variables = VariablesOf<typeof CompareProductsQuery>;
4445

45-
export const getCompareProducts = cache(
46-
async (variables: Variables, customerAccessToken?: string) => {
46+
const getCachedCompareProducts = unstable_cache(
47+
async (locale: string, variables: Variables) => {
4748
const parsedVariables = CompareProductsSchema.parse(variables);
4849

4950
if (parsedVariables.entityIds.length === 0) {
@@ -53,10 +54,36 @@ export const getCompareProducts = cache(
5354
const response = await client.fetch({
5455
document: CompareProductsQuery,
5556
variables: { ...parsedVariables, first: MAX_COMPARE_LIMIT },
56-
customerAccessToken,
57-
fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },
57+
locale,
58+
fetchOptions: { cache: 'no-store' },
5859
});
5960

6061
return removeEdgesAndNodes(response.data.site.products);
6162
},
63+
['compare-products-faceted'],
64+
{ revalidate },
65+
);
66+
67+
export const getCompareProducts = cache(
68+
async (locale: string, variables: Variables, customerAccessToken?: string) => {
69+
if (customerAccessToken) {
70+
const parsedVariables = CompareProductsSchema.parse(variables);
71+
72+
if (parsedVariables.entityIds.length === 0) {
73+
return [];
74+
}
75+
76+
const response = await client.fetch({
77+
document: CompareProductsQuery,
78+
variables: { ...parsedVariables, first: MAX_COMPARE_LIMIT },
79+
customerAccessToken,
80+
locale,
81+
fetchOptions: { cache: 'no-store' },
82+
});
83+
84+
return removeEdgesAndNodes(response.data.site.products);
85+
}
86+
87+
return getCachedCompareProducts(locale, variables);
88+
},
6289
);

core/app/[locale]/(default)/(faceted)/fetch-faceted-search.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';
2+
import { unstable_cache } from 'next/cache';
23
import { cache } from 'react';
34
import { z } from 'zod';
45

@@ -178,21 +179,52 @@ interface ProductSearch {
178179
filters: SearchProductsFiltersInput;
179180
}
180181

182+
const getCachedProductSearchResults = unstable_cache(
183+
async (locale: string, search: ProductSearch, currencyCode?: CurrencyCode) => {
184+
const { limit = 9, after, before, sort, filters } = search;
185+
const filterArgs = { filters, sort };
186+
const paginationArgs = before ? { last: limit, before } : { first: limit, after };
187+
188+
const response = await client.fetch({
189+
document: GetProductSearchResultsQuery,
190+
variables: { ...filterArgs, ...paginationArgs, currencyCode },
191+
locale,
192+
fetchOptions: { cache: 'no-store' },
193+
});
194+
195+
return response;
196+
},
197+
['product-search-results'],
198+
{ revalidate: 300 },
199+
);
200+
181201
const getProductSearchResults = cache(
182202
async (
203+
locale: string,
183204
{ limit = 9, after, before, sort, filters }: ProductSearch,
184205
currencyCode?: CurrencyCode,
185206
customerAccessToken?: string,
186207
) => {
187208
const filterArgs = { filters, sort };
188209
const paginationArgs = before ? { last: limit, before } : { first: limit, after };
189210

190-
const response = await client.fetch({
191-
document: GetProductSearchResultsQuery,
192-
variables: { ...filterArgs, ...paginationArgs, currencyCode },
193-
customerAccessToken,
194-
fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate: 300 } },
195-
});
211+
let response;
212+
213+
if (customerAccessToken) {
214+
response = await client.fetch({
215+
document: GetProductSearchResultsQuery,
216+
variables: { ...filterArgs, ...paginationArgs, currencyCode },
217+
customerAccessToken,
218+
locale,
219+
fetchOptions: { cache: 'no-store' },
220+
});
221+
} else {
222+
response = await getCachedProductSearchResults(
223+
locale,
224+
{ limit, after, before, sort, filters },
225+
currencyCode,
226+
);
227+
}
196228

197229
const { site } = response.data;
198230

@@ -406,13 +438,15 @@ export const PublicToPrivateParams = PublicSearchParamsSchema.catchall(SearchPar
406438
export const fetchFacetedSearch = cache(
407439
// We need to make sure the reference passed into this function is the same if we want it to be memoized.
408440
async (
441+
locale: string,
409442
params: z.input<typeof PublicSearchParamsSchema>,
410443
currencyCode?: CurrencyCode,
411444
customerAccessToken?: string,
412445
) => {
413446
const { after, before, limit = 9, sort, filters } = PublicToPrivateParams.parse(params);
414447

415448
return getProductSearchResults(
449+
locale,
416450
{
417451
after,
418452
before,

0 commit comments

Comments
 (0)