Releases: bigcommerce/catalyst
@bigcommerce/catalyst-makeswift@1.6.3
Patch Changes
- #2952
66d2a7dThanks @jorgemoya! - Upgrade@makeswift/runtimefrom0.26.3to0.26.4.
@bigcommerce/catalyst-core@1.6.2
Patch Changes
-
#2947
e198d89Thanks @jorgemoya! - Add root-level not-found page so /404 renders a branded page instead of the default Vercel error screen -
#2945
4479964Thanks @bc-svc-local! - Update translations.
@bigcommerce/create-catalyst@1.0.2
Patch Changes
- #2940
a4b614dThanks @copilot-swe-agent! - Align Node.js engine requirement with v24. Theengines.nodefield increate-catalystnow matches the runtime version gate (^24.0.0), ensuringpnpm create @bigcommerce/catalystcorrectly rejects unsupported Node versions before installation begins.
@bigcommerce/catalyst-makeswift@1.6.2
Patch Changes
- Pulls in changes from the
@bigcommerce/catalyst-core@1.6.1release. For more information about what was included in the@bigcommerce/catalyst-core@1.6.1release, see the changelog entry.
@bigcommerce/catalyst-core@1.6.1
Patch Changes
- #2934
6a5b019Thanks @chanceaclark! - Fix extra thick border on dropdown menu by changingring(3px) toring-1(1px) to match the Select component styling.
@bigcommerce/catalyst-makeswift@1.6.0
Minor Changes
- Pulls in changes from the
@bigcommerce/catalyst-core@1.6.0release. For more information about what was included in the@bigcommerce/catalyst-core@1.6.0release, see the changelog entry.
@bigcommerce/catalyst-core@1.6.0
Minor Changes
-
#2896
fc84210Thanks @jamesqquick! - Add reCAPTCHA v2 support to storefront forms. The reCAPTCHA widget is rendered on the registration, contact, and product review forms when enabled in the BigCommerce admin. All validation and error handling is performed server-side in the corresponding form actions. The token is read from the nativeg-recaptcha-responsefield that the widget injects into the form, eliminating the need for manual token extraction on the client.Migration steps
Step 1: Install dependencies
Add
react-google-recaptchaand its type definitions:pnpm add react-google-recaptcha pnpm add -D @types/react-google-recaptcha
Step 2: Add the reCAPTCHA server library
Create
core/lib/recaptcha/constants.ts:export interface ReCaptchaSettings { isEnabledOnStorefront: boolean; siteKey: string; } export const RECAPTCHA_TOKEN_FORM_KEY = 'g-recaptcha-response';
Create
core/lib/recaptcha.tswith the server-side helpers for fetching reCAPTCHA settings, extracting the token from form data, and asserting the token is present. See the file in this release for the full implementation.Step 3: Add reCAPTCHA translation strings
Update
core/messages/en.jsonto add therecaptchaRequiredmessage in each form namespace:"Auth": { "Register": { + "recaptchaRequired": "Please complete the reCAPTCHA verification.","Product": { "Reviews": { "Form": { + "recaptchaRequired": "Please complete the reCAPTCHA verification.","WebPages": { "ContactUs": { "Form": { + "recaptchaRequired": "Please complete the reCAPTCHA verification.","Form": { + "recaptchaRequired": "Please complete the reCAPTCHA verification.",Step 4: Update GraphQL mutations to accept reCAPTCHA token
Update
core/app/[locale]/(default)/(auth)/register/_actions/register-customer.ts:+ import { assertRecaptchaTokenPresent, getRecaptchaFromForm } from '~/lib/recaptcha'; ... const RegisterCustomerMutation = graphql(` - mutation RegisterCustomerMutation($input: RegisterCustomerInput!) { + mutation RegisterCustomerMutation( + $input: RegisterCustomerInput! + $reCaptchaV2: ReCaptchaV2Input + ) { customer { - registerCustomer(input: $input) { + registerCustomer(input: $input, reCaptchaV2: $reCaptchaV2) {
Update
core/app/[locale]/(default)/product/[slug]/_actions/submit-review.ts:+ import { assertRecaptchaTokenPresent, getRecaptchaFromForm } from '~/lib/recaptcha'; ... const AddProductReviewMutation = graphql(` - mutation AddProductReviewMutation($input: AddProductReviewInput!) { + mutation AddProductReviewMutation( + $input: AddProductReviewInput! + $reCaptchaV2: ReCaptchaV2Input + ) { catalog { - addProductReview(input: $input) { + addProductReview(input: $input, reCaptchaV2: $reCaptchaV2) {
Update
core/app/[locale]/(default)/webpages/[id]/contact/_actions/submit-contact-form.ts:+ import { assertRecaptchaTokenPresent, getRecaptchaFromForm } from '~/lib/recaptcha'; ... const SubmitContactUsMutation = graphql(` - mutation SubmitContactUsMutation($input: SubmitContactUsInput!) { - submitContactUs(input: $input) { + mutation SubmitContactUsMutation($input: SubmitContactUsInput!, $reCaptchaV2: ReCaptchaV2Input) { + submitContactUs(input: $input, reCaptchaV2: $reCaptchaV2) {
Step 5: Add server-side reCAPTCHA validation to form actions
In each of the three server actions above, add the validation block after the
parseWithZodcheck and pass the token to the GraphQL mutation. For example inregister-customer.ts:+ const { siteKey, token } = await getRecaptchaFromForm(formData); + const recaptchaValidation = assertRecaptchaTokenPresent(siteKey, token, t('recaptchaRequired')); + + if (!recaptchaValidation.success) { + return { + lastResult: submission.reply({ formErrors: recaptchaValidation.formErrors }), + }; + } ... const response = await client.fetch({ document: RegisterCustomerMutation, variables: { input, + reCaptchaV2: + recaptchaValidation.token != null ? { token: recaptchaValidation.token } : undefined, },
Apply the same pattern to
submit-review.tsandsubmit-contact-form.ts.Step 6: Pass
recaptchaSiteKeyto form componentsFetch the site key in each page and pass it down through the component tree.
Update
core/app/[locale]/(default)/(auth)/register/page.tsx:+ import { getRecaptchaSiteKey } from '~/lib/recaptcha'; ... + const recaptchaSiteKey = await getRecaptchaSiteKey(); ... <DynamicFormSection + recaptchaSiteKey={recaptchaSiteKey}
Update
core/app/[locale]/(default)/product/[slug]/page.tsx:+ import { getRecaptchaSiteKey } from '~/lib/recaptcha'; ... - const { product: baseProduct, settings } = await getProduct(productId, customerAccessToken); + const [{ product: baseProduct, settings }, recaptchaSiteKey] = await Promise.all([ + getProduct(productId, customerAccessToken), + getRecaptchaSiteKey(), + ]); ... <ProductDetail + recaptchaSiteKey={recaptchaSiteKey} ... <Reviews + recaptchaSiteKey={recaptchaSiteKey}
Update
core/app/[locale]/(default)/webpages/[id]/contact/page.tsx:+ import { getRecaptchaSiteKey } from '~/lib/recaptcha'; ... + const recaptchaSiteKey = await getRecaptchaSiteKey(); ... <DynamicForm + recaptchaSiteKey={recaptchaSiteKey}
Step 7: Render the reCAPTCHA widget in form components
Update
core/vibes/soul/form/dynamic-form/index.tsx:+ import RecaptchaWidget from 'react-google-recaptcha'; ... export interface DynamicFormProps<F extends Field> { + recaptchaSiteKey?: string; } ... + {recaptchaSiteKey ? <RecaptchaWidget sitekey={recaptchaSiteKey} /> : null}
Update
core/vibes/soul/sections/reviews/review-form.tsx:+ import RecaptchaWidget from 'react-google-recaptcha'; ... interface Props { + recaptchaSiteKey?: string; } ... + {recaptchaSiteKey ? ( + <div> + <RecaptchaWidget sitekey={recaptchaSiteKey} /> + </div> + ) : null}
Step 8: Thread
recaptchaSiteKeythrough intermediate componentsAdd the
recaptchaSiteKey?: stringprop and pass it through in:core/vibes/soul/sections/dynamic-form-section/index.tsxcore/vibes/soul/sections/product-detail/index.tsxcore/vibes/soul/sections/reviews/index.tsxcore/app/[locale]/(default)/product/[slug]/_components/reviews.tsx
Each of these accepts the prop and forwards it to the form component that renders the widget.
Patch Changes
- #2925
4e2f8f8Thanks @bc-svc-local! - Update translations.
@bigcommerce/catalyst-makeswift@1.5.0
Minor Changes
- Pulls in changes from the
@bigcommerce/catalyst-core@1.5.0release. For more information about what was included in the@bigcommerce/catalyst-core@1.5.0release, see the changelog entry.
Patch Changes
-
#2919
32be644Thanks @matthewvolk! - Upgrade@makeswift/runtimeto0.26.3, which uses the<Activity>API instead of<Suspense>. This fixes layout shift both when loading a Makeswift page directly and when client-side navigating from a non-Makeswift page to a Makeswift page after a hard refresh. -
#2862
52207b6Thanks @Codeseph! - fix: handle OPTIONS requests via MakeswiftApiHandler -
#2860
5097034Thanks @jorgemoya! - Add explicit Makeswift SEO metadata support to public-facing pages. When configured in Makeswift, the SEO title and description will take priority over the default values from BigCommerce or static translations.The following pages now support Makeswift SEO metadata:
- Home page (
/) - Catch-all page (
/[...rest]) - Product page (
/product/[slug]) - Brand page (
/brand/[slug]) - Category page (
/category/[slug]) - Blog list page (
/blog) - Blog post page (
/blog/[blogId]) - Search page (
/search) - Cart page (
/cart) - Compare page (
/compare) - Gift certificates page (
/gift-certificates) - Gift certificates balance page (
/gift-certificates/balance) - Contact webpage (
/webpages/[id]/contact) - Normal webpage (
/webpages/[id]/normal)
Migration steps
Step 1: Add
getMakeswiftPageMetadatafunctionAdd the
getMakeswiftPageMetadatafunction tocore/lib/makeswift/client.ts:+ export async function getMakeswiftPageMetadata({ path, locale }: { path: string; locale: string }) { + const { data: pages } = await client.getPages({ + pathPrefix: path, + locale: normalizeLocale(locale), + siteVersion: await getSiteVersion(), + }); + + if (pages.length === 0 || !pages[0]) { + return null; + } + + const { title, description } = pages[0]; + + return { + ...(title && { title }), + ...(description && { description }), + }; + }
Export the function from
core/lib/makeswift/index.ts:export { Page } from './page'; - export { client } from './client'; + export { client, getMakeswiftPageMetadata } from './client';Step 2: Update page metadata
Each page's
generateMetadatafunction has been updated to fetch Makeswift metadata and use it as the primary source, falling back to existing values. Here's an example using the cart page:Update
core/app/[locale]/(default)/cart/page.tsx:import { getPreferredCurrencyCode } from '~/lib/currency'; + import { getMakeswiftPageMetadata } from '~/lib/makeswift'; import { Slot } from '~/lib/makeswift/slot';export async function generateMetadata({ params }: Props): Promise<Metadata> { const { locale } = await params; const t = await getTranslations({ locale, namespace: 'Cart' }); + const makeswiftMetadata = await getMakeswiftPageMetadata({ path: '/cart', locale }); return { - title: t('title'), + title: makeswiftMetadata?.title || t('title'), + description: makeswiftMetadata?.description || undefined, }; }Apply the same pattern to the other pages listed above, using the appropriate path for each page (e.g.,
/blog,/search,/compare, etc.). - Home page (
@bigcommerce/catalyst-core@1.5.0
Minor Changes
-
#2905
6f788e9Thanks @matthewvolk! - Use dynamic imports for next/headers, next/navigation, and next-intl/server in the client module to avoid AsyncLocalStorage poisoning during next.config.ts resolution -
#2801
18cfdc8Thanks @Tharaae! - Fetch product inventory data with a separate GQL query with no cachingMigration
The files to be rebased for this change to be applied are:
- core/app/[locale]/(default)/product/[slug]/page-data.ts
- core/app/[locale]/(default)/product/[slug]/page.tsx
-
#2863
6a23c90Thanks @jorgemoya! - Add pagination support for the product gallery. When a product has more images than the initial page load, new images will load as batches once the user reaches the end of the existing thumbnails. Thumbnail images now will display in horizontal direction in all viewport sizes.Migration
- Create the new server action file
core/app/[locale]/(default)/product/[slug]/_actions/get-more-images.tswith a GraphQL query to fetch additional product images with pagination. - Update the product page data fetching in
core/app/[locale]/(default)/product/[slug]/page-data.tsto includepageInfo(withhasNextPageandendCursor) from the images query. - Update
core/app/[locale]/(default)/product/[slug]/page.tsxto pass the new pagination props (pageInfo,productId,loadMoreAction) to theProductDetailcomponent. - The
ProductGallerycomponent now accepts optional props for pagination:pageInfo?: { hasNextPage: boolean; endCursor: string | null }productId?: numberloadMoreAction?: ProductGalleryLoadMoreAction
Due to the number of changes, it is recommended to use the PR as a reference for migration.
- Create the new server action file
-
#2758
d78bc85Thanks @Tharaae! - Add the following messages to each line item on cart page based on store inventory settings:- Fully/partially out-of-stock message if enabled on the store and the line item is currently out of stock
- Ready-to-ship quantity if enabled on the store
- Backordered quantity if enabled on the store
Migration
For existing Catalyst stores, to get the newly added feature, simply rebase the existing code with the new release code. The files to be rebased for this change to be applied are:
- core/app/[locale]/(default)/cart/page-data.ts
- core/app/[locale]/(default)/cart/page.tsx
- core/messages/en.json
- core/vibes/soul/sections/cart/client.tsx
-
#2907
35adccbThanks @matthewvolk! - Upgrade Next.js to v16 and align peer dependencies.To migrate your Catalyst storefront to Next.js 16:
- Update
nextto^16.0.0in yourpackage.jsonand install dependencies. - Replace any usage of
unstable_expireTagwithrevalidateTagandunstable_expirePathwithrevalidatePathfromnext/cache. - Update
tsconfig.jsonto use"moduleResolution": "bundler"and"module": "nodenext"as required by Next.js 16. - Address Next.js 16 deprecation lint errors (e.g. legacy
<img>elements, missingrel="noopener noreferrer"on external links). - Rename
middleware.tstoproxy.tsand changeexport const middlewaretoexport const proxy(Next.js 16 proxy pattern). - Ensure you are running Node.js 24+ (proxy runs on the Node.js runtime, not Edge).
- Update
Patch Changes
-
#2916
e3185b6Thanks @chanceaclark! - Fix analytics visit count inflation by implementing a sliding window for the visit cookie TTL, guarding against prefetch/RSC requests creating spurious visits, and reordering middleware so analytics cookies survive locale redirects. -
#2852
a7395f1Thanks @chanceaclark! - Uses regulardompurify(DP) instead ofisomorphic-dompurify(IDP), because IDP requires JSDOM. JSDOM doesn't work in edge-runtime environments even with nodejs compatibility. We only need it on the client anyways for the JSON-LD schema, so it doesn't need the isomorphic aspect of it. This also changescore/app/[locale]/(default)/product/[slug]/_components/product-review-schema/product-review-schema.tsxto be a client-component to enable `dompurify to work correctly.Migration
- Remove the old dependency and add the new:
pnpm rm isomorphic-dompurify pnpm add dompurify -S
- Change the import in
core/app/[locale]/(default)/product/[slug]/_components/product-review-schema/product-review-schema.tsx:
- import DOMPurify from 'isomorphic-dompurify'; +// eslint-disable-next-line import/no-named-as-default +import DOMPurify from 'dompurify';
- Add the
'use client';directive to the top ofcore/app/[locale]/(default)/product/[slug]/_components/product-review-schema/product-review-schema.tsx.
-
#2844
74dee6eThanks @jordanarldt! - Update forms to translate the form field validation errorsMigration
Due to the amount of changes, it is recommended to just use the PR as a reference for migration.
Detailed migration steps can be found on the PR here:
#2844 -
#2901
8b5fee6Thanks @jordanarldt! - Fix GiftCertificateCard not updating when selecting a new amount on the gift certificate purchase form -
#2858
0633612Thanks @jorgemoya! - Use state abbreviation instead of entityId for cart shipping form state values. The shipping API expects state abbreviations, and using entityId caused form submissions to fail. Additionally, certain US military states that share the same abbreviation (AE) are now filtered out to prevent duplicate key issues and ambiguous submissions.Migration steps
Step 1: Add blacklist for states with duplicate abbreviations
Certain US states share the same abbreviation (AE), which causes issues with the shipping API and React select dropdowns. Add a blacklist to filter these out.
Update
core/app/[locale]/(default)/cart/page.tsx:const countries = shippingCountries.map((country) => ({ value: country.code, label: country.name, })); + // These US states share the same abbreviation (AE), which causes issues: + // 1. The shipping API uses abbreviations, so it can't distinguish between them + // 2. React select dropdowns require unique keys, causing duplicate key warnings + const blacklistedUSStates = new Set([ + 'Armed Forces Africa', + 'Armed Forces Canada', + 'Armed Forces Middle East', + ]); const statesOrProvinces = shippingCountries.map((country) => ({Step 2: Use state abbreviation instead of entityId
Update the state mapping to use
abbreviationinstead ofentityId, and apply the blacklist filter for US states.Update
core/app/[locale]/(default)/cart/page.tsx:const statesOrProvinces = shippingCountries.map((country) => ({ country: country.code, - states: country.statesOrProvinces.map((state) => ({ - value: state.entityId.toString(), - label: state.name, - })), + states: country.statesOrProvinces + .filter((state) => country.code !== 'US' || !blacklistedUSStates.has(state.name)) + .map((state) => ({ + value: state.abbreviation, + label: state.name, + })), })); -
#2856
f5330c7Thanks @jorgemoya! - Add canonical URLs and hreflang alternates for SEO. Pages now setalternates.canonicalandalternates.languagesingenerateMetadatavia the newgetMetadataAlternateshelper incore/lib/seo/canonical.ts. The helper fetches the vanity URL via GraphQL (site.settings.url.vanityUrl) and is cached per request. The default locale uses no path prefix; other locales use/{locale}/path. The root locale layout setsmetadataBaseto the configured vanity URL so canonical URLs resolve correctly. On Vercel preview deployments (VERCEL_ENV=preview),metadataBaseand canonical/hreflang URLs...
@bigcommerce/catalyst-makeswift@1.4.2
Patch Changes
- #2845
7f82903Thanks @chanceaclark! - Pulls in changes from the@bigcommerce/catalyst-core@1.4.2patch.