Skip to content

Releases: bigcommerce/catalyst

@bigcommerce/catalyst-makeswift@1.6.3

25 Mar 19:25
0037866

Choose a tag to compare

Patch Changes

@bigcommerce/catalyst-core@1.6.2

25 Mar 18:15
30ac2ab

Choose a tag to compare

Patch Changes

@bigcommerce/create-catalyst@1.0.2

20 Mar 18:54
c965f4d

Choose a tag to compare

Patch Changes

  • #2940 a4b614d Thanks @copilot-swe-agent! - Align Node.js engine requirement with v24. The engines.node field in create-catalyst now matches the runtime version gate (^24.0.0), ensuring pnpm create @bigcommerce/catalyst correctly rejects unsupported Node versions before installation begins.

@bigcommerce/catalyst-makeswift@1.6.2

20 Mar 19:30
91d8da7

Choose a tag to compare

Patch Changes

  • Pulls in changes from the @bigcommerce/catalyst-core@1.6.1 release. For more information about what was included in the @bigcommerce/catalyst-core@1.6.1 release, see the changelog entry.

@bigcommerce/catalyst-core@1.6.1

20 Mar 18:54
c965f4d

Choose a tag to compare

Patch Changes

  • #2934 6a5b019 Thanks @chanceaclark! - Fix extra thick border on dropdown menu by changing ring (3px) to ring-1 (1px) to match the Select component styling.

@bigcommerce/catalyst-makeswift@1.6.0

16 Mar 21:57
cd3d9ac

Choose a tag to compare

Minor Changes

  • Pulls in changes from the @bigcommerce/catalyst-core@1.6.0 release. For more information about what was included in the @bigcommerce/catalyst-core@1.6.0 release, see the changelog entry.

@bigcommerce/catalyst-core@1.6.0

16 Mar 21:23
a00b864

Choose a tag to compare

Minor Changes

  • #2896 fc84210 Thanks @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 native g-recaptcha-response field 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-recaptcha and 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.ts with 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.json to add the recaptchaRequired message 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 parseWithZod check and pass the token to the GraphQL mutation. For example in register-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.ts and submit-contact-form.ts.

    Step 6: Pass recaptchaSiteKey to form components

    Fetch 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 recaptchaSiteKey through intermediate components

    Add the recaptchaSiteKey?: string prop and pass it through in:

    • core/vibes/soul/sections/dynamic-form-section/index.tsx
    • core/vibes/soul/sections/product-detail/index.tsx
    • core/vibes/soul/sections/reviews/index.tsx
    • core/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

@bigcommerce/catalyst-makeswift@1.5.0

13 Mar 18:28
990727b

Choose a tag to compare

Minor Changes

  • Pulls in changes from the @bigcommerce/catalyst-core@1.5.0 release. For more information about what was included in the @bigcommerce/catalyst-core@1.5.0 release, see the changelog entry.

Patch Changes

  • #2919 32be644 Thanks @matthewvolk! - Upgrade @makeswift/runtime to 0.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 52207b6 Thanks @Codeseph! - fix: handle OPTIONS requests via MakeswiftApiHandler

  • #2860 5097034 Thanks @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 getMakeswiftPageMetadata function

    Add the getMakeswiftPageMetadata function to core/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 generateMetadata function 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.).

@bigcommerce/catalyst-core@1.5.0

13 Mar 15:38
0d951ab

Choose a tag to compare

Minor Changes

  • #2905 6f788e9 Thanks @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 18cfdc8 Thanks @Tharaae! - Fetch product inventory data with a separate GQL query with no caching

    Migration

    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 6a23c90 Thanks @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

    1. Create the new server action file core/app/[locale]/(default)/product/[slug]/_actions/get-more-images.ts with a GraphQL query to fetch additional product images with pagination.
    2. Update the product page data fetching in core/app/[locale]/(default)/product/[slug]/page-data.ts to include pageInfo (with hasNextPage and endCursor) from the images query.
    3. Update core/app/[locale]/(default)/product/[slug]/page.tsx to pass the new pagination props (pageInfo, productId, loadMoreAction) to the ProductDetail component.
    4. The ProductGallery component now accepts optional props for pagination:
      • pageInfo?: { hasNextPage: boolean; endCursor: string | null }
      • productId?: number
      • loadMoreAction?: ProductGalleryLoadMoreAction

    Due to the number of changes, it is recommended to use the PR as a reference for migration.

  • #2758 d78bc85 Thanks @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 35adccb Thanks @matthewvolk! - Upgrade Next.js to v16 and align peer dependencies.

    To migrate your Catalyst storefront to Next.js 16:

    • Update next to ^16.0.0 in your package.json and install dependencies.
    • Replace any usage of unstable_expireTag with revalidateTag and unstable_expirePath with revalidatePath from next/cache.
    • Update tsconfig.json to use "moduleResolution": "bundler" and "module": "nodenext" as required by Next.js 16.
    • Address Next.js 16 deprecation lint errors (e.g. legacy <img> elements, missing rel="noopener noreferrer" on external links).
    • Rename middleware.ts to proxy.ts and change export const middleware to export const proxy (Next.js 16 proxy pattern).
    • Ensure you are running Node.js 24+ (proxy runs on the Node.js runtime, not Edge).

Patch Changes

  • #2916 e3185b6 Thanks @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 a7395f1 Thanks @chanceaclark! - Uses regular dompurify (DP) instead of isomorphic-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 changes core/app/[locale]/(default)/product/[slug]/_components/product-review-schema/product-review-schema.tsx to be a client-component to enable `dompurify to work correctly.

    Migration

    1. Remove the old dependency and add the new:
    pnpm rm isomorphic-dompurify
    pnpm add dompurify -S
    1. 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';
    1. Add the 'use client'; directive to the top of core/app/[locale]/(default)/product/[slug]/_components/product-review-schema/product-review-schema.tsx.
  • #2844 74dee6e Thanks @jordanarldt! - Update forms to translate the form field validation errors

    Migration

    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 8b5fee6 Thanks @jordanarldt! - Fix GiftCertificateCard not updating when selecting a new amount on the gift certificate purchase form

  • #2858 0633612 Thanks @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 abbreviation instead of entityId, 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 f5330c7 Thanks @jorgemoya! - Add canonical URLs and hreflang alternates for SEO. Pages now set alternates.canonical and alternates.languages in generateMetadata via the new getMetadataAlternates helper in core/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 sets metadataBase to the configured vanity URL so canonical URLs resolve correctly. On Vercel preview deployments (VERCEL_ENV=preview), metadataBase and canonical/hreflang URLs...

Read more

@bigcommerce/catalyst-makeswift@1.4.2

27 Jan 21:14
5a33677

Choose a tag to compare

Patch Changes