Recco.js

Recco.js is a self-hosted JavaScript library which can be used to rapidly build an interactive, single-page-application user interface around the Luigi’s Box Recommender API.

You can integrate Luigi's Box Recommender by including the Recco.js script, setting configuration parameters and providing custom templates to customize the visual appearance.

Live demo
Basic example
Try the live example

Integration

By following this guide you will configure your site to use Recco.js to make request on your behalf to Luigi's Box Recommender API and display these recommendations back to your users. Since recommender is independent of what site is it included on you might use any site you have control over.

Example layout for the page

<html>
  <body>
    <!-- Header -->
    <!-- Product detail or other page content -->
    <div id="recommender-ui">
      <!-- Empty placeholder for recommender UI -->
    </div>
    <!-- Footer -->
  </body>
</html>

Prepare page for recommendations

Pick a page from your site that you would like to enrich with Luigi's Box Recommender – product pages, shopping carts or even home pages work best. Create an empty placeholder element where Recco.js will render recommender UI into. Note that if you pick element that is not empty, its contents will be replaced by the recommender UI.

For now we will use <div id="recommender-ui"></div>, however you can use any element or selector that fits you and your website.

<script type="text/x-template" id="..">
</script>
<script type="text/x-template" id="..">
</script>

<!-- Make sure that you define your templates before you initialize the Recco.js library -->
<script src="https://cdn.luigisbox.com/recco.js"></script>
<script>
  Luigis.Recommend({
    TrackerId: '2291-22343',
    Theme: 'luigis',
    Size: 5,
    Name: 'basket_detail',
    Type: 'basket_recommender'
  }, '#recommender-ui')
</script>

Setup Recco.js

Include the script and set configuration parameters. See the right column for an example.

Please note that:

  1. You must define your templates before you initialize Recco.js script. Templates are looked up when Recco.js first initializes and when they are not present in the page at that time, Recco.js will fall back to the default built-in templates.
  2. You must initialize the recommender by calling Luigis.Recommend. The initialization function takes 2 arguments: configuration object and CSS selector for the placeholder element where it will render the UI.
  3. You must define the initialization script (call to Luigis.Recommend) in the HTML after the placeholder element. The script expects to find the placeholder on initialization.

Without defining custom templates, you will get a very basic and unstyled recommender UI. You will most likely want to define custom templates where you can reuse your existing styles.

If you define the templates to match the HTML you are using today, there should be no extensive styling necessary.

Content Security Policy

If your website is using Content Security Policy, you need to add following rules to allow Luigi's Box Recco.js to work.

CSP directive Value
connect-src https://live.luigisbox.com
script-src https://cdn.luigisbox.com

Options reference

Luigis.Recommend accepts these arguments:

Option Comment  
TrackerIdREQUIRED Identifier of your site within Luigi's Box
Themeoptional Theme controls the visual style of the recommender UI. Recco.js currently supports 2 themes: 'default' and 'luigis'. We recommend that you start your integration with 'luigis' theme, and override template / style with CSS only what is necessary. See Theming for more details.
Sizeoptional Specifies how many results should the API return in each request (default: 10)
Type optional Unique identifier of a requested recommendation model. See Recommendation types for more details.
Name optional Name of the recommendations component. If you use multiple recommendation components on a single page they should be unique. Other values than "default" get appended to the template identifier, e.g. Name: "basket",... will make Recco.js look for template with selector #template-recommend-basket and #template-recommend-item-basket. (default: "default")
RecommenderClientId optional Arbitrary identifier by which you distinguish between different recommender placements. E.g., usage of last_seen recommender in homepage vs product detail page. (default: same value as Name)
GetItemIds optional Function which returns a list of items to base the recommendation on. Depending on the type of recommendation and placement it might be a list of resource identifiers of products in a shopping cart or category, list of resource identifiers of products from previous purchases, current resource identifier of a product user is exploring, etc. (defaults to empty list)
GetBlacklistedItemIds optional Function which returns a list of item resource identifiers that must not be recommended, e.g. different product variants that are very similar to ones returned by GetItemids or items which cannot be bought at the moment. (defaults to empty list)
Locale optional String, indicating a locale identifier which will setup the default translations and price format. See Localisation for more information.
Translations optional Object, including translation keys and translation themselves. See Localisation for more information.
PriceFilter optional Object, including configuration for price formatting. See Price format for more information.
OnDone optional Function called after results are rendered, with array of items as it's first parameter. For example, you can use this callback to initialize some kind of carousel.
OnItemsReady optional Function called after results are rendered, with context object as it's first parameter. This context is basically this in Vue.js. For example, you can access array of results with context.items, or DOM element of reccomender with context.$el.
RecommendationContext optional Function which returns a dict of restrictions for recommender identified in request time (e.g., filters used by user). See recommendation_context for more details.
SettingsOverride optional Function which returns a dict of selected settings in request time. See settings_override for more details.
HitFields optional Function which returns a list of fields. Only these fields (in addition to record identifier and type) will be retrieved and present in results. If not provided, all fields will be present in results. See hit_fields for more details.
CarouselOptions optional Object with settings for built-in carusel mode. See Carousel mode for more details.
Selector optional String containing CSS selector. If this option is specified, second argument of Luigis.Recommend can be omitted. See Batch mode for more details.
ModifyRequestParams optional Function to modify params of API call which recevies (params, state, getters) parameters, must return modified params object (default: undefined)
ModifyRequestData optional Function to modify data of API call which recevies (data, state, getters) parameters, must return modified data object (default: undefined)
PostponeDataCollection optional Boolean indicating whether data collection should be postponed after the OnDone function is called. See Postponing data collection for more information.

Luigis.Recommend also accepts a mandatory CSS selector for element where Luigi's Box recommender component should be rendered, e.g. #recommender-ui.

Templates

Luigi's Box Recco.js is using templates to render list of recommendations. While we include all templates in the default Recco.js distribution, they are not styled. Usually, you will want to define your custom template which matches the styling of your site. Templates are using Vue.js template syntax under the hood.

You should define these templates directly in your HTML code. Each template must be defined in its own <script type="text/x-template> tag. Templates are looked up by their id attribute — make sure to not change it. You don't have to redefine every template, only those that you will actually use.

Main template

Example of main recommendations template

<script type="text/x-template" id="template-recommend">
    <div>
        {{ $t('recommend.title', { name: name }) }}
        <recommend-item
            v-for="item in items"
            :key="name.concat('-').concat(item.url)"
            :item="item"
        ></recommend-item>
    </div>
</script>

This is the root template used for rendering recommendations layout. Use this template to define how your recommendation UI should look. You can reference a <recommend-item> component which renders individual recommended items.

If you set `Name` attribute when initializing reccomender, don't forget to use it as suffix when defining template. For example, Name: 'home-popular' and <script type="text/x-template" id="template-recommend-home-popular">...</script>

Recommended item component

Default recommend-item component definition

<script type="text/x-template" id="template-recommend-item">
    <div>
        <strong class="lb-default">Default Recommend Item</strong>
        <small>{{ item.url }}</small>
    </div>
</script>

Referenced as <recommend-item>.

Used for generating single recommended item. The default definition will render URL of each recommended item in a separate div with just the product URL. Override this template to render items in a custom structure, such as <ul> list or if you would like to display more details.

If you set `Name` attribute when initializing reccomender, don't forget to use it as suffix when defining template. For example, Name: 'home-popular' and <script type="text/x-template" id="template-recommend-item-home-popular">...</script>

Recipes

Theming

Recco.js comes with 2 themes which control the visual style of the recommender UI.

  1. luigis - which will give you a nicely styled list of recommendations. Use this theme, unless you have special requirements and plan to implement the recommender UI yourself from scratch.
  2. default - which is a barebone visual style, which only provides a very basic and unstyled UI. If you plan on implementing all templates by yourself, use this template.

Localisation

Localisation is controlled by 2 parameters: Translations which contains the translation keys and translated strings and PriceFilter, which controls the price format. There is also a 3rd parameter — Locale which just sets up built-in translations and price formats.

When localising the Recommender UI, we recommend that you configure Locale to load the defaults and then adjust translations or price format as necessary.

The locales supported out of the box are:

  • English (en)
  • Slovak (sk)

To load the default locale settings, use the country code, e.g. Locale: 'sk'.

Translations

Default English translations

{
    "recommend": {
        "title": "Recommended by Luigisbox - {name}"
    },
    "recommendItem": {
        "actionButton": "Detail",
        "availability": {
            "0": "Unavailable"
        }
    }
}

Translations are configured as a JavaScript object (JSON). See the defaults for English in the column with code examples. Note that the object that you pass to Translations is merged with the built-in translations. You can add new translations, you can override built-in translations, but you cannot delete a translation.

The Translations object must include the locales as the top-level keys. Note that you don't have to define the translation for all supported locales, but only for those that you care about.

For example, to override translation for action button in a product tile, set Translations: {"en": {"recommendItem": {"actionButton": "See product details"}}}.

If necessary, you can access the translation mechanism by calling a $t function from within the Recco.js templates. Pass it the translation key as an argument and additional parameters, e.g. {{ $t('recommend.title', {name: 'Example'}) }}.

You can also use $tc function which provides pluralization support. If you define translations like Translations: {"en": {"recommend": {"title": "0 recommendations | 1 recommendation | {hitsCount} recommendations"}}} you can use the pluralization function {{ $tc('recommend.title', hitsCount, {hitsCount}) }} which would output the translation based on hitsCount variable.

For more details see Vue I18n documentation.

Price format

Price format is controlled by a price filter, which is automatically called from within the default templates, such as {{ item.attributes.price_amount | price }}.

You can control the price filter options by setting PriceFilter parameter in the configuration options.

PriceFilter: {"decimals": 0, "prefixed": false, "symbol": "CZK"}
option description
decimals Specifies rounding precision
prefixed Boolean, specifies if the price symbol should be displayed before or after the price ($42 or 42$)
symbol The currency symbol

Batch mode

Live demo

Create Luigis.BatchRecommenders object

// populate settings
window.Luigis = window.Luigis || {};
window.Luigis.BatchRecommenders = {
  // settings is array of config objects
  settings: [
    {
      Selector: '#recommender-basket', // CSS selector for render element
      TrackerId: '2291-22343',
      Theme: 'luigis',
      Size: 5,
      Name: 'basket-similar', // name must be unique
      Type: 'basket_recommender'
    },
    {
      Selector: '#recommender-basket2', // CSS selector for render element
      TrackerId: '2291-22343',
      Theme: 'luigis',
      Size: 5,
      Name: 'basket-people-also-buy', // name must be unique
      Type: 'basket_recommender'
    }
  ]  
};

Instead of initializing each recommender one by one, you can use "batch mode". In this mode, only one API call is made for all recommenders and automatic deduplication is provided by backend. This means that if there are multiple recommenders on one page, recommended items in one recommender will not repeat in another recommender.

Settings for all recommender instances are stored in global Luigis.BatchRecommenders object, under settings key. It should be array of configuration objects. Name option must be present and unique for each recommender. For keeping all configuration in one place, you can use Selector option here instead of second argument (CSS selector for the placeholder element where it will render the UI) of Luigis.Recommend .

After setting Luigis.BatchRecommenders object you still need to initialize recommenders by calling Luigis.Recommend. You can do this by passing config object or string containing Name of recommender. If Selector option was set, second argument of Luigis.Recommend can be ommited.

Initialize recommenders

// init all recommenders
Luigis.BatchRecommenders.settings.forEach(function (settings){
  Luigis.Recommend(settings);
});

// or init single recommender
Luigis.Recommend('basket-similar'); // call with Name option

Carousel mode

Live demo

Use lbx-carousel in template

<script type="text/x-template" id="template-recommend">
    <div>
        {{ $t('recommend.title', { name: name }) }}
        <!--
        <recommend-item
            v-for="item in items"
            :key="name.concat('-').concat(item.url)"
            :item="item"
        ></recommend-item>
        -->
        <lbx-carousel v-if="items.length"></lbx-carousel>
    </div>
</script>

Recco.js has built-in carousel. If you want to use it, replace <recommend-item v-for="..." /> with <lbx-carousel /> component in your recommender template. Further customisation is possible by settings in CarouselOptions object. In this code example you can see possible options with their default values.

Set options for carousel

Luigis.Recommend({
    TrackerId: '2291-22343',
    Theme: 'luigis',
    Size: 5,
    Name: 'default',
    Type: 'basket_recommender',
    CarouselOptions: {
        // when true, styles will not be injected
        disableStyles: false,

        // sets var(--carousel-color) used in CSS
        color: '#000',

        // default class for carusel item
        itemClass: 'lb-recommend',

        // time in ms to auto move carousel
        // 0 means autoPlay is disabled
        autoPlay: 0,

        // when set to number, pagination will not work "per page" but "per slide" instead
        // for example, value 0 means that left (first) slide is active by default
        // see https://splidejs.com/guides/options/#focus
        focus: undefined,

        // display navigation arrows
        arrows: true,

        // display pagination
        pager: true,

        // number of items visible
        items: 3,

        // move by x items
        moveBy: 1,

        // if there are less recommender items than number of items visible, some items will be cloned to fill empty slots
        // when set to true, this behaviour is disabled
        limitItems: false,

        // loop items
        infinite: true,

        // alwas move by value set in moveBy option
        // if disabled, pagination works little differently
        forcePerMove: true,

        // pause autoPlay on focus
        pauseOnFocus: true,

        // pause autoPlay on hover
        pauseOnHover: true,

        // object with min-width values as keys
        // you can override options for breakpoints
        responsive: {
            480: {
                items: 1,
            },
        },
    },
  }, '#recommender-ui')

Pricing API integration

If you are using different pricing levels depending on the signed-in user, one of the strategies that you can use to render correct user prices in search is using your pricing API.

Search.js is written in Vue.js and that means that you can use the concept of reactivity to re-render prices after you load them from your API.

OnItemsReady: function(context) { var results = context.items;
  context.$store.commit('setItgState', {key: 'prices', data: null});

  if (results && results.length > 0) {
      // generate ids for API call
      var ids = [];
      if (results) {
          results.forEach(function(result) {
              if (result.attributes.item_id) {
                  ids.push(result.attributes.item_id);
              }
          });
      }

      var xhttp = new XMLHttpRequest();
      xhttp.onload = function() {
          // when we get API response
          // set itgState.prices to new prices from API
          // response is in format {id1: 9.99, id2: 19.99}
          var jsonParsed = JSON.parse(this.responseText);
          context.$store.commit('setItgState', {key: 'prices', data: jsonParsed});
      };

      // call API
      var apiUrl = 'https://www.example.com/pricing-api?ids='+ids.join(',');
      xhttp.open("GET", apiUrl);
      xhttp.send();
  }
}

The bulk of the code lives in the OnDone callback where you collect the identifiers of the results (in this example, item_id is used) and make an API request to your pricing API. When the XHR request completes, you set a special itgState reactive property and Vue.js will re-render the product tiles.

<div class="product-price">
    <span v-if="itgState.prices && itgState.prices[attributes.id]"> {{ itgState.prices[attributes.id] | price }}</span>
    <span v-else> {{ attributes.price_amount | price }}</span>
</div>

Template uses attributes.price_amount by default (feel free to use a loader element) and when the API call succeeds, Vue.js will automatically re-render component and use itgState.prices instead. You can use the xxx | price filter just like with price_amount.

Postponing Data Collection

To ensure accurate price collection in situations where the standard price_amount attribute is not available, utilize the PostponeDataCollection method. This may occur when interacting with the pricing API, leading to the complete absence of the price_amount attribute or when a different attribute, such as price_en_amount, is used.

Without access to the price_amount attribute, collecting prices for results accurately becomes impossible. This, in turn, results in the inability to measure cart value at all.

Upon activation, it employs an emitAnalyticsEventFn callback function passed to the onDone function. This enables you to provide the missing price_amount attribute in the OnDone function by assigning a price from the pricing API response or utilizing a different attribute that contains information about the resulting price.

OnDone: function(items, emitAnalyticsEventFn) {
  items.map(item => {
      // Add custom logic to retrieve the correct price
      item.attributes.price_amount = item.price_en_amount;
  })
  emitAnalyticsEventFn();
}