# Commercetools adapter

The commercetools adapter provides many features you may need in your Vue Storefront application that will prevent you from repeating the code in Vue templates.

# Features

# Automatic page number transformation

Adapter decrements page number before each request - so in the frontend, we have a human-friendly page's numeration starting from 1, while Algolia receives the page's number starting from 0.

# Category tree

An agnostic category tree (opens new window) is accessible using the getCategoryTree getter. The adapter makes an additional request to get proper slugs for categories in the tree.

import { useSearch, searchGetters } from '@vsf-enterprise/algolia';
import { AgnosticCategoryTree } from '@vue-storefront/core';

const { result } = useSearch();
const categoryTree: AgnosticCategoryTree = searchGetters.getCategoryTree(result.value);

# Pagination

An agnostic pagination object (opens new window) is accessible using the getPagination getter.

import { useSearch, searchGetters } from '@vsf-enterprise/algolia';
import { AgnosticPagination } from '@vue-storefront/core';

const { result } = useSearch();
const pagination: AgnosticPagination = searchGetters.getPagination(result.value);

# Sorting

An agnostic sort object (opens new window) is accessible using the getSortOptions getter.

import { useSearch, searchGetters } from '@vsf-enterprise/algolia';
import { AgnosticSort } from '@vue-storefront/core';

const { result } = useSearch();
const pagination: AgnosticSort = searchGetters.getSortOptions(result.value);

An agnostic breadcrumbs object (opens new window) is accessible using the getBreadcrumbs getter.

import { useSearch, searchGetters } from '@vsf-enterprise/algolia';
import { AgnosticBreadcrumb } from '@vue-storefront/core';

const { result } = useSearch();
const breadcrumbs: AgnosticBreadcrumb[]  = searchGetters.getBreadcrumbs(result.value);

# Filters

An agnostic filters object (opens new window) is accessible using the getFilters getter.

import { useSearch, searchGetters } from '@vsf-enterprise/algolia';
import { AgnosticFilter } from '@vue-storefront/core'; // It's in the @vsf-enterprise/algolia till VSF2.4 release

const { result } = useSearch();
const pagination: AgnosticFilter[] = searchGetters.getFilters(result.value);

# Hits

An agnostic hits object (opens new window) is accessible using the getItems getter.

import { useSearch, searchGetters } from '@vsf-enterprise/algolia';
import { AgnosticHit } from '@vsf-enterprise/algolia-types';

const { result } = useSearch();
const pagination: AgnosticHit[] = searchGetters.getItems(result.value);

# Requirements

# Strings

The adapter supports single- and multi-language structures.

The single-language structure is just a plain string:

const name = 'Pants';

However, the multi-language structure has a type of MultilangString, where the language key is stored as a key and translated string as a value.

type MultilangString = Record<string, string>;
const name: MultilangString = {
  en: 'Pants',
  de: 'Hose',
  pl: 'Spodnie'
};

# Product index

The most important index containing every product from commercetools. Thanks to it, it's possible to use every Algolia feature on the product's catalog.

WARNING

It's important to save each product variant as an individual record in Algolia. Their only shared trait is the parentId field.

const rawProduct = {
  name: {
    en: 'White Dress',
    pl: 'Biała sukienka'
  },
  categories: {
    en: [
      {
        slug: 'sale/women/dresses'
      },
      // ...
    ],
    pl: [
      {
        slug: 'wyprzedaz/damskie/suknie'
      },
      // ...
    ]
  },
  hierarchicalCategories: {
    en: [
      {
        lvl0: 'Sale',
        lvl1: 'Sale > Women',
        lvl2: 'Sale > Women > Dresses'
      },
      // ...
    ],
    pl: [
      {
        lvl0: 'Wyprzedaż',
        lvl1: 'Wyprzedaż > Damskie',
        lvl2: 'Wyprzedaż > Damskie > Suknie'
      },
      // ...
    ]
  },
  parentId: 'fdgf-408e-656-a127-dasd',
  slug: {
    en: 'white-dress',
    pl: 'biala-sukienka'
  },
  sku: 'MOE200000QD4',
  price: {
    type: 'centPrecision',
    currencyCode: 'USD',
    centAmount: 191950,
    fractionDigits: 2
  },
  images: [
    {
      url: 'https://some-website/abc.jpg'
    }
  ],
  color: 'white'
}

hierarchicalCategories - (Required) Path to the category with names joined with >
parentId - ID of variant's ancestor
sku - Variant SKU
price - currently, we support only one price so you have to apply logic of picking it in your indexer

After indexing, you have to add fields you want to filter by or populate facets from them to the attributesForFaceting (opens new window). For index shown above it will be:

  • color,
  • hierarchicalCategories.en.lvl0,
  • hierarchicalCategories.pl.lvl0,
  • hierarchicalCategories.en.lvl1,
  • hierarchicalCategories.pl.lvl1,
  • hierarchicalCategories.en.lvl2,
  • hierarchicalCategories.pl.lvl2,
  • categories.en.slug,
  • categories.pl.slug.

It's required to set the attributeForDistinct (opens new window) to the value of parentId.

# Category index

After fetching products, the adapter checks the index for details of products' categories. Based on the response, it builds a category tree with names, paths, and slugs.

const rawCategory = {
  name: {
    en: 'Dresses',
    pl: 'Suknie'
  },
  path: {
    en: 'Sale > Women > Dresses',
    pl: 'Wyprzedaż > Damskie > Suknie'
  },
  slug: {
    en: 'sale/sale-women/sale-women-dresses',
    pl: 'wyprzedaz/wyprzedaz-damskie/wyprzedaz-damskie-suknie'
  }
};

After indexing, you have to add the name, path, and slug for each language to the attributesForFaceting (opens new window). For index shown above it would be:

  • name.en,
  • name.pl,
  • path.en,
  • path.pl,
  • slug.en,
  • slug.pl.

# Commercetools Algolia Indexer

Instead of writing your own indexer, you might use an OpenSource commercetools-algolia-indexer (opens new window) library from ChangeCX.

# Adapter configuration

Open the middleware.config.js file and add filters to the attributesForFilters array, commercetools to the adapters, category entity with it's index to the entities, and defaultLocale. The latter is a code of the languages used as a fallback when locale cookie is missing. To use disjunctive faceting (opens new window) (perfect for multi-select filters), you can add them to the disjunctiveFacets array.

{
  algolia: {
  location: '@vsf-enterprise/algolia-api/server',
    configuration: {
      // ...
      entities: {
        product: {/*...*/},
        category: {
          name: {
            index: '<your_category_index>',
            default: true
          }
        }
      },
      defaultLocale: 'en',
      attributesForFilters: [
        'color'
      ],
      adapters: [
        'commercetools'
      ],
      disjunctiveFacets: [
        'color',
        'categories.en.slug',
        'categories.de.slug',
      ]
    }
  }
}

Then, open the nuxt.config.js file and add levels of hierarchical categories together with available filters' attributes.

['@vsf-enterprise/algolia/nuxt', {
  attributesForFilters: [
    'hierarchicalCategories.en.lvl0',
    'hierarchicalCategories.pl.lvl0',
    'hierarchicalCategories.en.lvl1',
    'hierarchicalCategories.pl.lvl1',
    'hierarchicalCategories.en.lvl2',
    'hierarchicalCategories.pl.lvl2',
    'color'
  ]
}]

# Example usage

# Query for Product Listing Page

import { useVSFContext } from '@vue-storefront/core';
import { useSearch } from '@vsf-enterprise/algolia';
import { useUiHelpers } from '~/composables';
//...
setup (context) {
  const th = useUiHelpers();
  const { $route } = context.root;
  const { search } = useSearch('cat-page');
  const { app: { i18n: { locale }} } = useVSFContext();

  onSSR(async () => {
    const { filters, page } = th.getFacetsFromURL();

    const facetFilters = Object.entries(filters).reduce((total, [ filterType, values ]) => {
      const filterValues = [];
      for (let value of values) {
        filterValues.push(`${filterType}:${value}`)
      }
      if (filterValues.length) {
        total.push(filterValues.length === 1 ? filterValues[0]: filterValues)
      }
      return total;
    }, []);

    await search({
      query: '',
      parameters: {
        page, 
        facetFilters: [
          ...facetFilters,
          `categories.${locale}.slug:${Object.values($route.params).filter(Boolean).join('/')}`
        ]
      },
      sort: $route.query.sort
    });
  });
}
import { useSearch } from '@vsf-enterprise/algolia';
import { useVSFContext } from '@vue-storefront/core';
//...
setup (context) {
  const { search } = useSearch('search-bar-query');
  const { app: { i18n: { locale }} } = useVSFContext();

  const onSearch = async (textQueryInSearchbar) => {
    await search({ 
      query: textQueryInSearchbar,
      parameters: {
        facets: [`hierarchicalCategories.${locale}.lvl0`, `hierarchicalCategories.${locale}.lvl1`]
      }
    });
  };
}