Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4ca18e2
refactor(core): use dynamic imports in client to avoid AsyncLocalStor…
matthewvolk Mar 3, 2026
fbf8169
chore(core): bump next.js to 16 and align peer deps
matthewvolk Feb 24, 2026
d1728c1
refactor(core): replace unstable_expireTag with revalidateTag
matthewvolk Feb 25, 2026
8d9dee6
refactor(core): replace unstable_expirePath with revalidatePath
matthewvolk Feb 24, 2026
4f272d3
chore(core): update tsconfig for next.js 16
matthewvolk Feb 24, 2026
06c4b38
fix(core): resolve Next.js 16 deprecation lint errors
matthewvolk Mar 3, 2026
c50e7a7
feat(core): add X-Correlation-ID header, cache guest queries separately
jorgemoya Mar 3, 2026
107398b
fix: use unstable_cache
jorgemoya Mar 4, 2026
b30cc4c
fix: remove no-cache
jorgemoya Mar 4, 2026
12a6dfd
feat: enable cache components
jorgemoya Mar 4, 2026
5fd25a6
fix: remove force-dynamic in route
jorgemoya Mar 4, 2026
011c437
fix: remove correlation-id
jorgemoya Mar 4, 2026
ca89e5f
fix: issue with Date
jorgemoya Mar 4, 2026
347cab3
fix: issues with cache components
jorgemoya Mar 4, 2026
ae3714f
fix: suspense toast notifications
jorgemoya Mar 4, 2026
e995454
fix: use counter instead of uuid in streamable
jorgemoya Mar 4, 2026
2ad0586
fix: more stream fixes
jorgemoya Mar 4, 2026
8492c99
fix: cache extra queries
jorgemoya Mar 5, 2026
eccc847
fix: cache reviews
jorgemoya Mar 5, 2026
ac9f5c6
fix: wrap pages in Suspense
jorgemoya Mar 5, 2026
04b48de
fix: lint
jorgemoya Mar 5, 2026
b2c790f
fix: wrap footer
jorgemoya Mar 5, 2026
1850912
fix: uncache metadata queries
jorgemoya Mar 5, 2026
96e84c5
fix: debug
jorgemoya Mar 5, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ coverage/
.history
.unlighthouse
.bigcommerce
.worktrees
13 changes: 11 additions & 2 deletions core/app/[locale]/(default)/(auth)/change-password/page-data.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { cacheLife } from 'next/cache';
import { cache } from 'react';

import { client } from '~/client';
Expand All @@ -24,10 +25,14 @@ const ChangePasswordQuery = graphql(`
}
`);

export const getChangePasswordQuery = cache(async () => {
async function getCachedChangePasswordQuery(locale: string) {
'use cache';

cacheLife({ revalidate });

const response = await client.fetch({
document: ChangePasswordQuery,
fetchOptions: { next: { revalidate } },
locale,
});

const passwordComplexitySettings =
Expand All @@ -36,4 +41,8 @@ export const getChangePasswordQuery = cache(async () => {
return {
passwordComplexitySettings,
};
}

export const getChangePasswordQuery = cache(async (locale: string) => {
return getCachedChangePasswordQuery(locale);
});
26 changes: 20 additions & 6 deletions core/app/[locale]/(default)/(auth)/change-password/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable react/jsx-no-bind */
import { Metadata } from 'next';
import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Suspense } from 'react';

import { ResetPasswordSection } from '@/vibes/soul/sections/reset-password-section';
import { getChangePasswordQuery } from '~/app/[locale]/(default)/(auth)/change-password/page-data';
Expand All @@ -26,19 +27,20 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
};
}

export default async function ChangePassword({ params, searchParams }: Props) {
const { locale } = await params;

setRequestLocale(locale);
interface ContentProps {
searchParams: Promise<{ c?: string; t?: string }>;
locale: string;
}

async function ChangePasswordContent({ searchParams, locale }: ContentProps) {
const { c: customerEntityId, t: token } = await searchParams;
const t = await getTranslations('Auth.ChangePassword');
const t = await getTranslations({ locale, namespace: 'Auth.ChangePassword' });

if (!customerEntityId || !token) {
return redirect({ href: '/login', locale });
}

const { passwordComplexitySettings } = await getChangePasswordQuery();
const { passwordComplexitySettings } = await getChangePasswordQuery(locale);

return (
<ResetPasswordSection
Expand All @@ -50,3 +52,15 @@ export default async function ChangePassword({ params, searchParams }: Props) {
/>
);
}

export default async function ChangePassword({ params, searchParams }: Props) {
const { locale } = await params;

setRequestLocale(locale);

return (
<Suspense>
<ChangePasswordContent locale={locale} searchParams={searchParams} />
</Suspense>
);
}
18 changes: 15 additions & 3 deletions core/app/[locale]/(default)/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PropsWithChildren } from 'react';
import { setRequestLocale } from 'next-intl/server';
import { PropsWithChildren, Suspense } from 'react';

import { isLoggedIn } from '~/auth';
import { redirect } from '~/i18n/routing';
Expand All @@ -7,13 +8,24 @@ interface Props extends PropsWithChildren {
params: Promise<{ locale: string }>;
}

export default async function Layout({ children, params }: Props) {
async function AuthCheck({ locale, children }: { locale: string; children: React.ReactNode }) {
const loggedIn = await isLoggedIn();
const { locale } = await params;

if (loggedIn) {
redirect({ href: '/account/orders', locale });
}

return children;
}

export default async function Layout({ children, params }: Props) {
const { locale } = await params;

setRequestLocale(locale);

return (
<Suspense>
<AuthCheck locale={locale}>{children}</AuthCheck>
</Suspense>
);
}
26 changes: 21 additions & 5 deletions core/app/[locale]/(default)/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable react/jsx-no-bind */
import { Metadata } from 'next';
import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Suspense } from 'react';

import { ButtonLink } from '@/vibes/soul/primitives/button-link';
import { SignInSection } from '@/vibes/soul/sections/sign-in-section';
Expand All @@ -27,13 +28,16 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
};
}

export default async function Login({ params, searchParams }: Props) {
const { locale } = await params;
async function LoginContent({
searchParams,
locale,
}: {
searchParams: Props['searchParams'];
locale: string;
}) {
const { redirectTo = '/account/orders', error } = await searchParams;

setRequestLocale(locale);

const t = await getTranslations('Auth.Login');
const t = await getTranslations({ locale, namespace: 'Auth.Login' });

const vanityUrl = buildConfig.get('urls').vanityUrl;
const redirectUrl = new URL(redirectTo, vanityUrl);
Expand Down Expand Up @@ -75,3 +79,15 @@ export default async function Login({ params, searchParams }: Props) {
</>
);
}

export default async function Login({ params, searchParams }: Props) {
const { locale } = await params;

setRequestLocale(locale);

return (
<Suspense>
<LoginContent locale={locale} searchParams={searchParams} />
</Suspense>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,3 @@ export async function GET(_: Request, { params }: { params: Promise<{ token: str
redirect(`/login?error=InvalidToken`);
}
}

export const dynamic = 'force-dynamic';
16 changes: 11 additions & 5 deletions core/app/[locale]/(default)/(auth)/register/page-data.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { cacheLife } from 'next/cache';
import { cache } from 'react';

import { getSessionCustomerAccessToken } from '~/auth';
import { client } from '~/client';
import { graphql, VariablesOf } from '~/client/graphql';
import { revalidate } from '~/client/revalidate-target';
import { FormFieldsFragment } from '~/data-transformers/form-field-transformer/fragment';

const RegisterCustomerQuery = graphql(
Expand Down Expand Up @@ -61,8 +62,10 @@ interface Props {
};
}

export const getRegisterCustomerQuery = cache(async ({ address, customer }: Props) => {
const customerAccessToken = await getSessionCustomerAccessToken();
async function getCachedRegisterCustomerQuery(locale: string, { address, customer }: Props) {
'use cache';

cacheLife({ revalidate });

const response = await client.fetch({
document: RegisterCustomerQuery,
Expand All @@ -72,8 +75,7 @@ export const getRegisterCustomerQuery = cache(async ({ address, customer }: Prop
customerFilters: customer?.filters,
customerSortBy: customer?.sortBy,
},
fetchOptions: { cache: 'no-store' },
customerAccessToken,
locale,
});

const addressFields = response.data.site.settings?.formFields.shippingAddress;
Expand All @@ -92,4 +94,8 @@ export const getRegisterCustomerQuery = cache(async ({ address, customer }: Prop
countries,
passwordComplexitySettings,
};
}

export const getRegisterCustomerQuery = cache(async (locale: string, props: Props) => {
return getCachedRegisterCustomerQuery(locale, props);
});
2 changes: 1 addition & 1 deletion core/app/[locale]/(default)/(auth)/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default async function Register({ params }: Props) {

const t = await getTranslations('Auth.Register');

const registerCustomerData = await getRegisterCustomerQuery({
const registerCustomerData = await getRegisterCustomerQuery(locale, {
address: { sortBy: 'SORT_ORDER' },
customer: { sortBy: 'SORT_ORDER' },
});
Expand Down
45 changes: 41 additions & 4 deletions core/app/[locale]/(default)/(faceted)/brand/[slug]/page-data.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { cacheLife } from 'next/cache';
import { cache } from 'react';

import { client } from '~/client';
Expand Down Expand Up @@ -38,13 +39,49 @@ const BrandPageQuery = graphql(`
}
`);

export const getBrandPageData = cache(async (entityId: number, customerAccessToken?: string) => {
// Don't cache metadata
export const getBrandPageMetadata = cache(
async (locale: string, entityId: number, customerAccessToken?: string) => {
const response = await client.fetch({
document: BrandPageQuery,
variables: { entityId },
customerAccessToken,
locale,
fetchOptions: { cache: 'no-store' },
});

return response.data.site;
},
);

async function getCachedBrandPageData(locale: string, entityId: number) {
'use cache';

cacheLife({ revalidate });

const response = await client.fetch({
document: BrandPageQuery,
variables: { entityId },
customerAccessToken,
fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },
locale,
});

return response.data.site;
});
}

export const getBrandPageData = cache(
async (locale: string, entityId: number, customerAccessToken?: string) => {
if (customerAccessToken) {
const response = await client.fetch({
document: BrandPageQuery,
variables: { entityId },
customerAccessToken,
locale,
fetchOptions: { cache: 'no-store' },
});

return response.data.site;
}

return getCachedBrandPageData(locale, entityId);
},
);
41 changes: 30 additions & 11 deletions core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { getFormatter, getTranslations, setRequestLocale } from 'next-intl/server';
import { createLoader, SearchParams } from 'nuqs/server';
import { cache } from 'react';
import { cache, Suspense } from 'react';

import { Streamable } from '@/vibes/soul/lib/streamable';
import { createCompareLoader } from '@/vibes/soul/primitives/compare-drawer/loader';
Expand All @@ -19,7 +19,7 @@ import { MAX_COMPARE_LIMIT } from '../../../compare/page-data';
import { getCompareProducts as getCompareProductsData } from '../../fetch-compare-products';
import { fetchFacetedSearch } from '../../fetch-faceted-search';

import { getBrandPageData } from './page-data';
import { getBrandPageData, getBrandPageMetadata } from './page-data';

const getCachedBrand = cache((brandId: string) => {
return {
Expand All @@ -30,9 +30,14 @@ const getCachedBrand = cache((brandId: string) => {
const compareLoader = createCompareLoader();

const createBrandSearchParamsLoader = cache(
async (brandId: string, customerAccessToken?: string) => {
async (locale: string, brandId: string, customerAccessToken?: string) => {
const cachedBrand = getCachedBrand(brandId);
const brandSearch = await fetchFacetedSearch(cachedBrand, undefined, customerAccessToken);
const brandSearch = await fetchFacetedSearch(
locale,
cachedBrand,
undefined,
customerAccessToken,
);
const brandFacets = brandSearch.facets.items.filter(
(facet) => facet.__typename !== 'BrandSearchFilter',
);
Expand Down Expand Up @@ -73,7 +78,7 @@ export async function generateMetadata(props: Props): Promise<Metadata> {

const brandId = Number(slug);

const { brand } = await getBrandPageData(brandId, customerAccessToken);
const { brand } = await getBrandPageMetadata(locale, brandId, customerAccessToken);

if (!brand) {
return notFound();
Expand All @@ -89,7 +94,7 @@ export async function generateMetadata(props: Props): Promise<Metadata> {
};
}

export default async function Brand(props: Props) {
async function BrandContent(props: Props) {
const { locale, slug } = await props.params;
const customerAccessToken = await getSessionCustomerAccessToken();

Expand All @@ -99,7 +104,7 @@ export default async function Brand(props: Props) {

const brandId = Number(slug);

const { brand, settings } = await getBrandPageData(brandId, customerAccessToken);
const { brand, settings } = await getBrandPageData(locale, brandId, customerAccessToken);

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

const loadSearchParams = await createBrandSearchParamsLoader(slug, customerAccessToken);
const loadSearchParams = await createBrandSearchParamsLoader(locale, slug, customerAccessToken);
const parsedSearchParams = loadSearchParams?.(searchParams) ?? {};

const search = await fetchFacetedSearch(
locale,
{
...searchParams,
...parsedSearchParams,
Expand Down Expand Up @@ -162,10 +168,15 @@ export default async function Brand(props: Props) {

const streamableFilters = Streamable.from(async () => {
const searchParams = await props.searchParams;
const loadSearchParams = await createBrandSearchParamsLoader(slug, customerAccessToken);
const loadSearchParams = await createBrandSearchParamsLoader(locale, slug, customerAccessToken);
const parsedSearchParams = loadSearchParams?.(searchParams) ?? {};
const cachedBrand = getCachedBrand(slug);
const categorySearch = await fetchFacetedSearch(cachedBrand, undefined, customerAccessToken);
const categorySearch = await fetchFacetedSearch(
locale,
cachedBrand,
undefined,
customerAccessToken,
);
const refinedSearch = await streamableFacetedSearch;

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

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

const products = await getCompareProductsData(compareIds, customerAccessToken);
const products = await getCompareProductsData(locale, compareIds, customerAccessToken);

return products.map((product) => ({
id: product.entityId.toString(),
Expand Down Expand Up @@ -244,3 +255,11 @@ export default async function Brand(props: Props) {
/>
);
}

export default function Brand(props: Props) {
return (
<Suspense>
<BrandContent {...props} />
</Suspense>
);
}
Loading
Loading